searchApiInput.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState";
  2. import { ApiClientError } from "@/lib/frontend/apiClient";
  3. import { getSearchDateRangeValidation } from "@/lib/frontend/search/searchDateValidation";
  4. function isNonEmptyString(value) {
  5. return typeof value === "string" && value.trim().length > 0;
  6. }
  7. function toTrimmedOrNull(value) {
  8. return isNonEmptyString(value) ? value.trim() : null;
  9. }
  10. function buildValidationError(code, message, details) {
  11. return new ApiClientError({
  12. status: 400,
  13. code,
  14. message,
  15. details,
  16. });
  17. }
  18. /**
  19. * Build the apiClient.search(...) input from URL state + current user context.
  20. *
  21. * Return shape:
  22. * - input: object for apiClient.search(...) or null (no search yet / not ready)
  23. * - error: ApiClientError or null (local validation / fast-fail)
  24. *
  25. * UX policy for MULTI without branches:
  26. * - Treat it as "not ready" (input=null, error=null) instead of an error.
  27. *
  28. * @param {{
  29. * urlState: {
  30. * q: string|null,
  31. * scope: "single"|"multi"|"all",
  32. * branch: string|null,
  33. * branches: string[],
  34. * from: string|null,
  35. * to: string|null
  36. * },
  37. * routeBranch: string,
  38. * user: { role: string, branchId: string|null }|null,
  39. * cursor?: string|null,
  40. * limit?: number
  41. * }} args
  42. * @returns {{ input: any|null, error: any|null }}
  43. */
  44. export function buildSearchApiInput({
  45. urlState,
  46. routeBranch,
  47. user,
  48. cursor = null,
  49. limit = 100,
  50. }) {
  51. const q = isNonEmptyString(urlState?.q) ? urlState.q.trim() : null;
  52. // UI policy (RHL-024): q is required to trigger a search.
  53. if (!q) return { input: null, error: null };
  54. // --- Date range validation (RHL-025) ------------------------------------
  55. const from = toTrimmedOrNull(urlState?.from);
  56. const to = toTrimmedOrNull(urlState?.to);
  57. const dateValidation = getSearchDateRangeValidation(from, to);
  58. if (dateValidation) {
  59. return {
  60. input: null,
  61. error: buildValidationError(
  62. dateValidation.code,
  63. dateValidation.message,
  64. dateValidation.details
  65. ),
  66. };
  67. }
  68. // --- Build input ---------------------------------------------------------
  69. const input = { q, limit };
  70. if (from) input.from = from;
  71. if (to) input.to = to;
  72. if (isNonEmptyString(cursor)) input.cursor = cursor.trim();
  73. const role = user?.role;
  74. // Branch users: always restricted to the current route branch.
  75. if (role === "branch") {
  76. input.branch = routeBranch;
  77. return { input, error: null };
  78. }
  79. // Admin/dev: respect scope rules from URL state.
  80. if (role === "admin" || role === "dev") {
  81. if (urlState.scope === SEARCH_SCOPE.ALL) {
  82. input.scope = "all";
  83. return { input, error: null };
  84. }
  85. if (urlState.scope === SEARCH_SCOPE.MULTI) {
  86. const branches = Array.isArray(urlState.branches)
  87. ? urlState.branches
  88. : [];
  89. // UX: missing branches is not an error, it's "not ready".
  90. if (branches.length === 0) {
  91. return { input: null, error: null };
  92. }
  93. input.scope = "multi";
  94. input.branches = branches;
  95. return { input, error: null };
  96. }
  97. // SINGLE
  98. input.branch = routeBranch;
  99. return { input, error: null };
  100. }
  101. // Unknown role: fail-safe to single-branch using the route context.
  102. input.branch = routeBranch;
  103. return { input, error: null };
  104. }