pdfUrl.js 2.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. /**
  2. * PDF URL helpers for the Explorer (RHL-023).
  3. *
  4. * Why this file exists:
  5. * - The PDF endpoint is a binary stream and must NOT be called via apiClient (JSON-centric).
  6. * - We want centralized, testable URL construction for:
  7. * GET /api/files/:branch/:year/:month/:day/:filename
  8. *
  9. * Design goals:
  10. * - Pure functions (no React, no window, no Next runtime).
  11. * - Defensive encoding for filename (spaces, #, unicode, etc.).
  12. * - Minimal, predictable behavior.
  13. */
  14. /**
  15. * Ensure a required string input is present and not empty/whitespace.
  16. *
  17. * Note:
  18. * - We validate emptiness using trim(), but we may return the original value
  19. * to avoid mutating semantics (important for filenames).
  20. *
  21. * @param {string} name
  22. * @param {unknown} value
  23. * @returns {string}
  24. */
  25. function requireNonEmptyString(name, value) {
  26. if (typeof value !== "string") {
  27. throw new Error(`Route segment "${name}" must be a string`);
  28. }
  29. if (!value.trim()) {
  30. throw new Error(`Route segment "${name}" must not be empty`);
  31. }
  32. return value;
  33. }
  34. /**
  35. * Encode a standard route segment (branch/year/month/day).
  36. * - We normalize by trimming.
  37. *
  38. * @param {string} name
  39. * @param {unknown} value
  40. * @returns {string}
  41. */
  42. function encodeSegment(name, value) {
  43. return encodeURIComponent(requireNonEmptyString(name, value).trim());
  44. }
  45. /**
  46. * Encode a filename segment.
  47. * - We do NOT trim the filename (filenames are filesystem-exact).
  48. * - We still validate that it isn't empty/whitespace.
  49. *
  50. * @param {unknown} value
  51. * @returns {string}
  52. */
  53. function encodeFilename(value) {
  54. return encodeURIComponent(requireNonEmptyString("filename", value));
  55. }
  56. /**
  57. * Build the PDF stream URL (inline by default).
  58. *
  59. * @param {{
  60. * branch: string,
  61. * year: string,
  62. * month: string,
  63. * day: string,
  64. * filename: string
  65. * }} input
  66. * @returns {string}
  67. */
  68. export function buildPdfUrl({ branch, year, month, day, filename }) {
  69. const b = encodeSegment("branch", branch);
  70. const y = encodeSegment("year", year);
  71. const m = encodeSegment("month", month);
  72. const d = encodeSegment("day", day);
  73. const f = encodeFilename(filename);
  74. return `/api/files/${b}/${y}/${m}/${d}/${f}`;
  75. }
  76. /**
  77. * Build the PDF download URL (forces Content-Disposition: attachment).
  78. *
  79. * @param {{
  80. * branch: string,
  81. * year: string,
  82. * month: string,
  83. * day: string,
  84. * filename: string
  85. * }} input
  86. * @returns {string}
  87. */
  88. export function buildPdfDownloadUrl(input) {
  89. return `${buildPdfUrl(input)}?download=1`;
  90. }