Newer
Older
express-blog / src / utils / processMenuLinks.js
// src/utils/processMenuLinks.js
const { winstonLogger } = require("./logging/index.js");
const { evaluateRules } = require("../utils/evaluateRules.js");

/**
 * Applies attribute promotion from a child to a parent.
 */
function promoteAttributes(parent, child) {
  const promote = child.promote;
  if (!promote) return;

  let keys = [];

  if (promote === "true" || promote === true) {
    keys = Object.keys(child).filter((k) => k !== "submenu" && k !== "promote");
  } else if (typeof promote === "string") {
    keys = [promote];
  } else if (Array.isArray(promote)) {
    keys = promote;
  }

  keys.forEach((key) => {
    parent[key] = child[key];
  });
}
function normalizePath(item, currentPath) {
  if (item.appendCurrentPath && typeof item.href === "string") {
    const shouldAppend =
      currentPath !== "/" && !item.href.endsWith(currentPath);
    if (shouldAppend) item.href += currentPath;
  }

  if (item.html || item.frame || item.mermaid) {
    item.href = `/docs/hexa/${item.html || item.frame || item.mermaid}`;
  }
  return item;
}

function handleSubmenuLogic(item, session, currentPath, activePolicy) {
  const nextPolicy = activePolicy === "deny-children" ? "deny" : activePolicy;
  const processedSub = processMenuLinks(
    item.submenu,
    session,
    currentPath,
    nextPolicy,
  );

  if (processedSub.length === 0) {
    delete item.submenu;
    return item;
  }

  const primaryChild = item.submenu.find((s) => s.promote);
  const isPrimaryVisible = processedSub.some(
    (s) => s.label === primaryChild?.label,
  );

  if (isPrimaryVisible && primaryChild?.promote) {
    promoteAttributes(item, primaryChild);
    // Remove the redundant link from dropdown if it was promoted to the parent
    item.submenu = processedSub.filter((s) => s.label !== primaryChild.label);
  } else {
    item.submenu = processedSub;
  }

  if (item.submenu.length === 0) delete item.submenu;
  return item;
}

/**
 * Processes menu links with policy inheritance and parent-override logic.
 * @param {Array} links - The menu items to filter.
 * @param {Object} session - The { isAuthenticated, user, groups } object.
 * @param {string} currentPath - The active URL path.
 * @param {string} inheritedPolicy - The policy passed down from the parent (default "allow").
 */

function processMenuLinks(
  links,
  session,
  currentPath,
  inheritedPolicy = "allow",
) {
  return (links || []).reduce((acc, link) => {
    let item = { ...link };
    const activePolicy = item.policy || inheritedPolicy;

    if (item.submenu) {
      item = handleSubmenuLogic(item, session, currentPath, activePolicy);
    }

    item = normalizePath(item, currentPath);

    const finalPolicy = item.policy || inheritedPolicy;
    const isVisible =
      finalPolicy === "allow" ||
      finalPolicy === "deny-children" ||
      (finalPolicy === "deny" &&
        session.isAuthenticated &&
        evaluateRules(item.rules, session));

    if (isVisible) acc.push(item);
    return acc;
  }, []);
}

module.exports = processMenuLinks;