@medusajs/product
Version:
Medusa Product module
405 lines • 20.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductCategoryRepository = void 0;
const utils_1 = require("@medusajs/framework/utils");
const core_1 = require("@mikro-orm/core");
const _models_1 = require("../models");
// eslint-disable-next-line max-len
class ProductCategoryRepository extends utils_1.DALUtils.MikroOrmBaseTreeRepository {
buildFindOptions(findOptions = { where: {} }, familyOptions = {}) {
const findOptions_ = { ...findOptions };
findOptions_.options ??= {};
findOptions_.options.orderBy = {
id: "ASC",
rank: "ASC",
...findOptions_.options.orderBy,
};
const fields = (findOptions_.options.fields ??= []);
const populate = (findOptions_.options.populate ??= []);
// Ref: Building descendants
// mpath and parent_category_id needs to be added to the query for the tree building to be done accurately
if (familyOptions.includeDescendantsTree ||
familyOptions.includeAncestorsTree) {
fields.indexOf("mpath") === -1 && fields.push("mpath");
fields.indexOf("parent_category_id") === -1 &&
fields.push("parent_category_id");
}
const shouldExpandParent = familyOptions.includeAncestorsTree ||
populate.includes("parent_category") ||
fields.some((field) => field.startsWith("parent_category."));
if (shouldExpandParent) {
populate.indexOf("parent_category") === -1 &&
populate.push("parent_category");
}
const shouldExpandChildren = familyOptions.includeDescendantsTree ||
populate.includes("category_children") ||
fields.some((field) => field.startsWith("category_children."));
if (shouldExpandChildren) {
populate.indexOf("category_children") === -1 &&
populate.push("category_children");
}
Object.assign(findOptions_.options, {
strategy: core_1.LoadStrategy.SELECT_IN,
});
return findOptions_;
}
async find(findOptions = { where: {} }, transformOptions = {}, context = {}) {
const manager = super.getActiveManager(context);
const findOptions_ = this.buildFindOptions(findOptions, transformOptions);
const productCategories = await manager.find(_models_1.ProductCategory.name, findOptions_.where, { ...findOptions_.options } // TODO
);
if (!transformOptions.includeDescendantsTree &&
!transformOptions.includeAncestorsTree) {
return productCategories;
}
const categoriesTree = await this.buildProductCategoriesWithTree({
descendants: transformOptions.includeDescendantsTree,
ancestors: transformOptions.includeAncestorsTree,
}, productCategories, findOptions_, context);
return this.sortCategoriesByRank(categoriesTree);
}
sortCategoriesByRank(categories) {
const sortedCategories = categories.sort((a, b) => a.rank - b.rank);
for (const category of sortedCategories) {
if (category.category_children) {
// All data up to this point is manipulated as an array, but it is a Collection<ProductCategory> type under the hood, so we are casting to any here.
category.category_children = this.sortCategoriesByRank(category.category_children);
}
}
return sortedCategories;
}
async buildProductCategoriesWithTree(include, productCategories, findOptions = { where: {} }, context = {}) {
const { serialize = true } = findOptions;
delete findOptions.serialize;
const manager = super.getActiveManager(context);
// We dont want to get the relations as we will fetch all the categories and build the tree manually
let relationIndex = findOptions.options?.populate?.indexOf("parent_category") ?? -1;
const shouldPopulateParent = relationIndex !== -1;
if (shouldPopulateParent && include.ancestors) {
findOptions.options.populate.splice(relationIndex, 1);
}
relationIndex =
findOptions.options?.populate?.indexOf("category_children") ?? -1;
const shouldPopulateChildren = relationIndex !== -1;
if (shouldPopulateChildren && include.descendants) {
findOptions.options.populate.splice(relationIndex, 1);
}
const mpaths = [];
const parentMpaths = new Set();
for (const cat of productCategories) {
if (include.descendants) {
mpaths.push({ mpath: { $like: `${cat.mpath}%` } });
}
if (include.ancestors) {
let parent = "";
cat.mpath?.split(".").forEach((mpath) => {
parentMpaths.add(parent + mpath);
parent += mpath + ".";
});
}
}
mpaths.push({ mpath: Array.from(parentMpaths) });
const where = { ...findOptions.where, $or: mpaths };
const options = {
...findOptions.options,
limit: undefined,
offset: 0,
};
delete where.id;
delete where.mpath;
delete where.parent_category_id;
const categoriesInTree = serialize
? await this.serialize(await manager.find(_models_1.ProductCategory.name, where, options))
: await manager.find(_models_1.ProductCategory.name, where, options);
const categoriesById = new Map(categoriesInTree.map((cat) => [cat.id, cat]));
categoriesInTree.forEach((cat) => {
if (cat.parent_category_id && include.ancestors) {
cat.parent_category = categoriesById.get(cat.parent_category_id);
cat.parent_category_id = categoriesById.get(cat.parent_category_id)?.["id"];
}
});
const populateChildren = (category, level = 0) => {
const categories = categoriesInTree.filter((child) => child.parent_category_id === category.id);
if (include.descendants) {
category.category_children = categories.map((child) => {
return populateChildren(categoriesById.get(child.id), level + 1);
});
}
if (level === 0) {
if (!include.ancestors && !shouldPopulateParent) {
delete category.parent_category;
}
return category;
}
if (include.ancestors) {
delete category.category_children;
}
if (include.descendants) {
delete category.parent_category;
}
return category;
};
const populatedProductCategories = productCategories.map((cat) => {
const fullCategory = categoriesById.get(cat.id);
return populateChildren(fullCategory);
});
return populatedProductCategories;
}
async findAndCount(findOptions = { where: {} }, transformOptions = {}, context = {}) {
const manager = super.getActiveManager(context);
const findOptions_ = this.buildFindOptions(findOptions, transformOptions);
const [productCategories, count] = (await manager.findAndCount(_models_1.ProductCategory.name, findOptions_.where, findOptions_.options));
if (!transformOptions.includeDescendantsTree &&
!transformOptions.includeAncestorsTree) {
return [productCategories, count];
}
const categoriesTree = await this.buildProductCategoriesWithTree({
descendants: transformOptions.includeDescendantsTree,
ancestors: transformOptions.includeAncestorsTree,
}, productCategories, findOptions_, context);
return [this.sortCategoriesByRank(categoriesTree), count];
}
async delete(ids, context = {}) {
const manager = super.getActiveManager(context);
await this.baseDelete(ids, context);
await manager.nativeDelete(_models_1.ProductCategory.name, { id: ids }, {});
return ids;
}
async softDelete(ids, context = {}) {
const manager = super.getActiveManager(context);
await this.baseDelete(ids, context);
const categories = await Promise.all(ids.map(async (id) => {
const productCategory = await manager.findOne(_models_1.ProductCategory.name, {
id,
});
if (!productCategory) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `ProductCategory with id: ${id} was not found`);
}
manager.assign(productCategory, { deleted_at: new Date() });
return productCategory;
}));
manager.persist(categories);
return [categories, {}];
}
async restore(ids, context = {}) {
const manager = super.getActiveManager(context);
const categories = await Promise.all(ids.map(async (id) => {
const productCategory = await manager.findOneOrFail(_models_1.ProductCategory.name, {
id,
});
manager.assign(productCategory, { deleted_at: null });
return productCategory;
}));
manager.persist(categories);
return [categories, {}];
}
async baseDelete(ids, context = {}) {
const manager = super.getActiveManager(context);
await Promise.all(ids.map(async (id) => {
const productCategory = await manager.findOne(_models_1.ProductCategory.name, { id }, {
populate: ["category_children"],
} // TODO
);
if (!productCategory) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `ProductCategory with id: ${id} was not found`);
}
if (productCategory.category_children.length > 0) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_ALLOWED, `Deleting ProductCategory (${id}) with category children is not allowed`);
}
await this.rerankSiblingsAfterDeletion(manager, productCategory);
}));
}
async create(data, context = {}) {
const manager = super.getActiveManager(context);
const categories = await Promise.all(data.map(async (entry, i) => {
const categoryData = {
...entry,
};
const siblingsCount = await manager.count(_models_1.ProductCategory.name, {
parent_category_id: categoryData?.parent_category_id || null,
});
categoryData.rank ??= siblingsCount + i;
if (categoryData.rank > siblingsCount + i) {
categoryData.rank = siblingsCount + i;
}
// There is no need to rerank if it is the last item in the list
if (categoryData.rank < siblingsCount + i) {
await this.rerankSiblingsAfterCreation(manager, categoryData);
}
// Set the base mpath if the category has a parent. The model `create` hook will append the own id to the base mpath.
let parentCategory = null;
const parentCategoryId = categoryData.parent_category_id ?? categoryData.parent_category?.id;
if (parentCategoryId) {
parentCategory = await manager.findOne(_models_1.ProductCategory.name, parentCategoryId);
if (!parentCategory) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_ARGUMENT, `Parent category with id: '${parentCategoryId}' does not exist`);
}
}
const result = await manager.create(_models_1.ProductCategory.name, categoryData);
/**
* Since "mpath" calculation relies on the id of the created
* category, we have to compute it after calling manager.create. So
* that we can access the "category.id" which is under the hood
* defined by DML.
*/
manager.assign(result, {
mpath: parentCategory
? `${parentCategory.mpath}.${result.id}`
: result.id,
});
return result;
}));
manager.persist(categories);
return categories;
}
async update(data, context = {}) {
const manager = super.getActiveManager(context);
const categories = await Promise.all(data.map(async (entry, i) => {
const categoryData = {
...entry,
};
let productCategory = await manager.findOne(_models_1.ProductCategory.name, {
id: categoryData.id,
});
if (!productCategory) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.NOT_FOUND, `ProductCategory with id: ${categoryData.id} was not found`);
}
// If the parent or rank are not changed, no need to reorder anything.
if (!(0, utils_1.isDefined)(categoryData.parent_category_id) &&
!(0, utils_1.isDefined)(categoryData.rank)) {
for (const key in categoryData) {
if ((0, utils_1.isDefined)(categoryData[key])) {
productCategory[key] = categoryData[key];
}
}
manager.assign(productCategory, categoryData);
return productCategory;
}
// If the parent is changed, we need to rerank the siblings of the old parent and the new parent.
if ((0, utils_1.isDefined)(categoryData.parent_category_id) &&
categoryData.parent_category_id !== productCategory.parent_category_id) {
// Calculate the new mpath
if (categoryData.parent_category_id === null) {
categoryData.mpath = "";
}
else {
productCategory = (await this.buildProductCategoriesWithTree({
descendants: true,
}, [productCategory], {
where: { id: productCategory.id },
serialize: false,
}, context))[0];
const newParentCategory = await manager.findOne(_models_1.ProductCategory.name, categoryData.parent_category_id);
if (!newParentCategory) {
throw new utils_1.MedusaError(utils_1.MedusaError.Types.INVALID_ARGUMENT, `Parent category with id: '${categoryData.parent_category_id}' does not exist`);
}
const categoryDataChildren = categoryData.category_children?.flatMap((child) => child.category_children ?? []);
const categoryDataChildrenMap = new Map(categoryDataChildren?.map((child) => [child.id, child]));
function updateMpathRecursively(category, newBaseMpath) {
const newMpath = `${newBaseMpath}.${category.id}`;
category.mpath = newMpath;
for (let child of category.category_children) {
child = manager.getReference(_models_1.ProductCategory.name, child.id);
manager.assign(child, categoryDataChildrenMap.get(child.id) ?? {});
updateMpathRecursively(child, newMpath);
}
}
updateMpathRecursively(productCategory, newParentCategory.mpath);
// categoryData.mpath = `${newParentCategory.mpath}.${productCategory.id}`
}
// Rerank the siblings in the new parent
const siblingsCount = await manager.count(_models_1.ProductCategory.name, {
parent_category_id: categoryData.parent_category_id,
});
categoryData.rank ??= siblingsCount + i;
if (categoryData.rank > siblingsCount + i) {
categoryData.rank = siblingsCount + i;
}
// There is no need to rerank if it is the last item in the list
if (categoryData.rank < siblingsCount + i) {
await this.rerankSiblingsAfterCreation(manager, categoryData);
}
// Rerank the old parent's siblings
await this.rerankSiblingsAfterDeletion(manager, productCategory);
for (const key in categoryData) {
if ((0, utils_1.isDefined)(categoryData[key])) {
productCategory[key] = categoryData[key];
}
}
manager.assign(productCategory, categoryData);
return productCategory;
// If only the rank changed, we need to rerank all siblings.
}
else if ((0, utils_1.isDefined)(categoryData.rank)) {
const siblingsCount = await manager.count(_models_1.ProductCategory.name, {
parent_category_id: productCategory.parent_category_id,
});
// Subtracting 1 since we don't count the modified category itself
if (categoryData.rank > siblingsCount - 1 + i) {
categoryData.rank = siblingsCount - 1 + i;
}
await this.rerankAllSiblings(manager, productCategory, categoryData);
}
for (const key in categoryData) {
if ((0, utils_1.isDefined)(categoryData[key])) {
productCategory[key] = categoryData[key];
}
}
manager.assign(productCategory, categoryData);
return productCategory;
}));
manager.persist(categories);
return categories;
}
async rerankSiblingsAfterDeletion(manager, removedSibling) {
const affectedSiblings = await manager.find(_models_1.ProductCategory.name, {
parent_category_id: removedSibling.parent_category_id,
rank: { $gt: removedSibling.rank },
});
const updatedSiblings = affectedSiblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank - 1 });
return sibling;
});
manager.persist(updatedSiblings);
}
async rerankSiblingsAfterCreation(manager, addedSibling) {
const affectedSiblings = await manager.find(_models_1.ProductCategory.name, {
parent_category_id: addedSibling.parent_category_id,
rank: { $gte: addedSibling.rank },
});
const updatedSiblings = affectedSiblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank + 1 });
return sibling;
});
manager.persist(updatedSiblings);
}
async rerankAllSiblings(manager, originalSibling, updatedSibling) {
if (originalSibling.rank === updatedSibling.rank) {
return;
}
if (originalSibling.rank < updatedSibling.rank) {
const siblings = await manager.find(_models_1.ProductCategory.name, {
parent_category_id: originalSibling.parent_category_id,
rank: { $gt: originalSibling.rank, $lte: updatedSibling.rank },
}, { orderBy: { rank: "ASC" } });
const updatedSiblings = siblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank - 1 });
return sibling;
});
manager.persist(updatedSiblings);
}
else {
const siblings = await manager.find(_models_1.ProductCategory.name, {
parent_category_id: originalSibling.parent_category_id,
rank: { $gte: updatedSibling.rank, $lt: originalSibling.rank },
}, { orderBy: { rank: "ASC" } });
const updatedSiblings = siblings.map((sibling) => {
manager.assign(sibling, { rank: sibling.rank + 1 });
return sibling;
});
manager.persist(updatedSiblings);
}
}
}
exports.ProductCategoryRepository = ProductCategoryRepository;
//# sourceMappingURL=product-category.js.map
;