| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- import { ApiClientError } from "@/lib/frontend/apiClient";
- import { SEARCH_SCOPE } from "@/lib/frontend/search/urlState";
- function isNonEmptyString(value) {
- return typeof value === "string" && value.trim().length > 0;
- }
- /**
- * Build the apiClient.search(...) input from URL state + current user context.
- *
- * Why this exists:
- * - Search UI state is URL-driven and shareable.
- * - Cursor is intentionally kept out of the URL by default (client state only).
- * - Role/scoping rules must be enforced consistently (branch users are always single-branch).
- *
- * Return shape:
- * - input: object for apiClient.search(...) or null (no search yet)
- * - error: ApiClientError or null (local validation / fast-fail)
- *
- * @param {{
- * urlState: {
- * q: string|null,
- * scope: "single"|"multi"|"all",
- * branch: string|null,
- * branches: string[],
- * from: string|null,
- * to: string|null
- * },
- * routeBranch: string,
- * user: { role: string, branchId: string|null }|null,
- * cursor?: string|null,
- * limit?: number
- * }} args
- * @returns {{ input: any|null, error: ApiClientError|null }}
- */
- export function buildSearchApiInput({
- urlState,
- routeBranch,
- user,
- cursor = null,
- limit = 100,
- }) {
- const q = isNonEmptyString(urlState?.q) ? urlState.q.trim() : null;
- // No query => no search request. UI should show an "idle" empty state.
- if (!q) return { input: null, error: null };
- const input = { q, limit };
- // Keep from/to as pass-through for RHL-025 (future).
- if (isNonEmptyString(urlState?.from)) input.from = urlState.from.trim();
- if (isNonEmptyString(urlState?.to)) input.to = urlState.to.trim();
- if (isNonEmptyString(cursor)) input.cursor = cursor.trim();
- const role = user?.role;
- // Branch users: always restricted to the current route branch.
- if (role === "branch") {
- input.branch = routeBranch;
- return { input, error: null };
- }
- // Admin/dev: respect scope rules from URL state.
- if (role === "admin" || role === "dev") {
- if (urlState.scope === SEARCH_SCOPE.ALL) {
- input.scope = "all";
- return { input, error: null };
- }
- if (urlState.scope === SEARCH_SCOPE.MULTI) {
- const branches = Array.isArray(urlState.branches)
- ? urlState.branches
- : [];
- if (branches.length === 0) {
- return {
- input: null,
- error: new ApiClientError({
- status: 400,
- code: "VALIDATION_SEARCH_BRANCHES",
- message: "Missing branches parameter for multi scope",
- }),
- };
- }
- input.scope = "multi";
- input.branches = branches;
- return { input, error: null };
- }
- // SINGLE
- input.branch = routeBranch;
- return { input, error: null };
- }
- // Unknown role: fail-safe to single-branch using the route context.
- input.branch = routeBranch;
- return { input, error: null };
- }
|