export type QueryParams = Record< string, | string | number | boolean | string[] | null | undefined | Record >; type Method = "get" | "post" | "put" | "delete" | "patch" | "head" | "options"; let urlDefaults: Record = {}; export type RouteDefinition = { url: string; } & (TMethod extends Method[] ? { methods: TMethod } : { method: TMethod }); export type RouteFormDefinition = { action: string; method: TMethod; }; export type RouteQueryOptions = { query?: QueryParams; mergeQuery?: QueryParams; }; export const queryParams = (options?: RouteQueryOptions) => { if (!options || (!options.query && !options.mergeQuery)) { return ""; } const query = options.query ?? options.mergeQuery; const includeExisting = options.mergeQuery !== undefined; const getValue = (value: string | number | boolean) => { if (value === true) { return "1"; } if (value === false) { return "0"; } return value.toString(); }; const params = new URLSearchParams( includeExisting && typeof window !== "undefined" ? window.location.search : "", ); for (const key in query) { if (query[key] === undefined || query[key] === null) { params.delete(key); continue; } if (Array.isArray(query[key])) { if (params.has(`${key}[]`)) { params.delete(`${key}[]`); } query[key].forEach((value) => { params.append(`${key}[]`, value.toString()); }); } else if (typeof query[key] === "object") { params.forEach((_, paramKey) => { if (paramKey.startsWith(`${key}[`)) { params.delete(paramKey); } }); for (const subKey in query[key]) { if (typeof query[key][subKey] === "undefined") { continue; } if ( ["string", "number", "boolean"].includes( typeof query[key][subKey], ) ) { params.set( `${key}[${subKey}]`, getValue(query[key][subKey]), ); } } } else { params.set(key, getValue(query[key])); } } const str = params.toString(); return str.length > 0 ? `?${str}` : ""; }; export const setUrlDefaults = (params: Record) => { urlDefaults = params; }; export const addUrlDefault = ( key: string, value: string | number | boolean, ) => { urlDefaults[key] = value; }; export const applyUrlDefaults = | undefined>( existing: T, ): T => { const existingParams = { ...(existing ?? ({} as Record)) }; for (const key in urlDefaults) { if ( existingParams[key] === undefined && urlDefaults[key] !== undefined ) { (existingParams as Record)[key] = urlDefaults[key]; } } return existingParams as T; }; export const validateParameters = ( args: Record | undefined, optional: string[], ) => { const missing = optional.filter((key) => !args?.[key]); const expectedMissing = optional.slice(missing.length * -1); for (let i = 0; i < missing.length; i++) { if (missing[i] !== expectedMissing[i]) { throw Error( "Unexpected optional parameters missing. Unable to generate a URL.", ); } } };