Newer
Older
express-blog / src / controllers / docsControllers.js
const fs = require("fs/promises");
const path = require("path");
const { createHash } = require("crypto");
const { qualifyLink } = require("../utils/qualifyLinks");
const { baseUrl } = require("../utils/baseUrl");
const HttpError = require("../utils/HttpError");
const docsContext = require("../utils/docsContext");
const {
  loadDocFile,
  filterModuleSecurityKeys,
  getYamlFileNames,
  docsDir,
} = require("../services/docsService");

function computeHash(input) {
  return createHash("sha1").update(input).digest("hex");
}

async function checkDocCache(req, fileNamesOrPath, namespace = "") {
  const files = Array.isArray(fileNamesOrPath)
    ? fileNamesOrPath
    : [fileNamesOrPath];

  const statPaths = await Promise.all(
    files.map(async (f) => {
      const filePath = path.join(docsDir, f + ".yaml");
      try {
        const stat = await fs.stat(filePath);
        return stat;
      } catch (err) {
        throw err;
      }
    })
  );

  const lastModified = new Date(
    Math.max(...statPaths.map((s) => s.mtimeMs))
  ).toUTCString();

  const hashBase = Array.isArray(fileNamesOrPath)
    ? fileNamesOrPath.join(",")
    : fileNamesOrPath;

  const etag = `"${computeHash(namespace + hashBase)}"`;

  const result = req.checkCacheHeaders({ etag, lastModified });

  return result;
}

exports.renderDocsIndex = async (req, res, next) => {
  try {
    const yamlFiles = await getYamlFileNames();

    // Read from cache
    if (await checkDocCache(req, yamlFiles)) return;

    const context = await docsContext(req.isAuthenticated, {
      layout: "docs",
      docPath: "/docs",
      docModule: null,
    });

    res.render("docs/index", {
      ...context,
      docsPaths: yamlFiles.map((name) => `${req.baseUrl || ""}/${name}`),
    });
  } catch (err) {
    next(new HttpError("Failed to read docs directory", 500, err.stack));
  }
};

exports.renderDocsSummary = async (req, res, next) => {
  try {
    const summaries = [];
    const yamlFiles = await getYamlFileNames();

    // Read from cache
    if (await checkDocCache(req, yamlFiles, "summary")) return;

    for (const file of yamlFiles) {
      const doc = await loadDocFile(file);
      if (doc?.crossCuttingSummary) {
        summaries.push({ path: file, summary: doc.crossCuttingSummary });
      }
    }

    const context = await docsContext(req.isAuthenticated, {
      layout: "docs",
      docPath: baseUrl + "/docs/summary",
      docModule: null,
    });

    res.render("docs/summary", {
      ...context,
      summaries,
    });
  } catch (err) {
    next(err);
  }
};

exports.renderDocsByType = async (req, res, next) => {
  const { moduleType: docPath } = req.params;

  // Read from cache
  if (await checkDocCache(req, docPath)) return;

  const doc = await loadDocFile(docPath);

  const context = await docsContext(req.isAuthenticated, {
    layout: "docs",
    docPath: baseUrl + "/docs" + docPath,
    docModule: null,
  });

  const modulesWithLinks = Object.entries(doc.modules).map(([key]) => ({
    name: key,
    url: `${baseUrl}/docs/${docPath}/${key}`,
  }));

  res.render("docs/path", {
    ...context,
    docsHome: qualifyLink("/docs"),
    pathName: docPath,
    crossCuttingSummary: doc.crossCuttingSummary,
    modules: modulesWithLinks,
  });
};

exports.renderDocsModule = async (req, res, next) => {
  const { moduleType: docPath, module } = req.params;

  // Read from cache
  if (await checkDocCache(req, docPath)) return;

  const doc = await loadDocFile(docPath);
  if (!doc) return next(new HttpError("Documentation not found", 404));

  const moduleDoc = doc.modules[module];
  if (!moduleDoc)
    return next(new HttpError("Module documentation not found", 404));

  const context = await docsContext(req.isAuthenticated, {
    layout: "docs",
    docPath,
    docModule: module,
  });

  res.render("docs/module", {
    ...context,
    docsHome: qualifyLink("/docs"),
    pathUrl: qualifyLink("/docs/" + docPath),
    pathName: docPath,
    module,
    moduleDoc: filterModuleSecurityKeys(moduleDoc),
  });
};