searchApiInput.js 2.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState";
  2. import { buildDateFilterValidationError } from "@/lib/frontend/search/dateFilterValidation";
  3. import { normalizeIsoDateYmdOrNull } from "@/lib/frontend/search/dateRange";
  4. function isNonEmptyString(value) {
  5. return typeof value === "string" && value.trim().length > 0;
  6. }
  7. /**
  8. * Build the apiClient.search(...) input from URL state + current user context.
  9. *
  10. * Return shape:
  11. * - input: object for apiClient.search(...) or null (no search yet / not ready)
  12. * - error: ApiClientError or null (local validation / fast-fail)
  13. *
  14. * UX policy for MULTI without branches:
  15. * - Treat it as "not ready" (input=null, error=null) instead of an error.
  16. */
  17. export function buildSearchApiInput({
  18. urlState,
  19. routeBranch,
  20. user,
  21. cursor = null,
  22. limit = 100,
  23. }) {
  24. const q = isNonEmptyString(urlState?.q) ? urlState.q.trim() : null;
  25. // UI policy (RHL-024): q is required to trigger a search.
  26. if (!q) return { input: null, error: null };
  27. // DRY: validate date filters via shared pure helper (RHL-025 UX + consistency).
  28. const dateErr = buildDateFilterValidationError({
  29. from: urlState?.from ?? null,
  30. to: urlState?.to ?? null,
  31. });
  32. if (dateErr) return { input: null, error: dateErr };
  33. const input = { q, limit };
  34. // Normalize (trim + validity) before passing to apiClient.
  35. const from = normalizeIsoDateYmdOrNull(urlState?.from);
  36. const to = normalizeIsoDateYmdOrNull(urlState?.to);
  37. if (from) input.from = from;
  38. if (to) input.to = to;
  39. if (isNonEmptyString(cursor)) input.cursor = cursor.trim();
  40. const role = user?.role;
  41. // Branch users: always restricted to the current route branch.
  42. if (role === "branch") {
  43. input.branch = routeBranch;
  44. return { input, error: null };
  45. }
  46. // Admin/dev: respect scope rules from URL state.
  47. if (role === "admin" || role === "dev") {
  48. if (urlState.scope === SEARCH_SCOPE.ALL) {
  49. input.scope = "all";
  50. return { input, error: null };
  51. }
  52. if (urlState.scope === SEARCH_SCOPE.MULTI) {
  53. const branches = Array.isArray(urlState.branches)
  54. ? urlState.branches
  55. : [];
  56. // UX: missing branches is not an error, it's "not ready".
  57. if (branches.length === 0) {
  58. return { input: null, error: null };
  59. }
  60. input.scope = "multi";
  61. input.branches = branches;
  62. return { input, error: null };
  63. }
  64. // SINGLE
  65. input.branch = routeBranch;
  66. return { input, error: null };
  67. }
  68. // Unknown role: fail-safe to single-branch using the route context.
  69. input.branch = routeBranch;
  70. return { input, error: null };
  71. }