-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathsearchParams.ts
More file actions
118 lines (98 loc) · 3.41 KB
/
Copy pathsearchParams.ts
File metadata and controls
118 lines (98 loc) · 3.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import type { ZodType } from "zod";
import { z } from "zod";
import { fromZodError } from "zod-validation-error";
/**
* Parses a comma-separated `runIds` query param into a trimmed, de-duplicated
* list of run friendly IDs, capped at 100. Shared by the runs `/live` and
* `/children-statuses` resource routes.
*/
export const runIdsQueryParam = z
.string()
.optional()
.transform((value) => {
const ids =
value
?.split(",")
.map((id) => id.trim())
.filter(Boolean) ?? [];
return [...new Set(ids)].slice(0, 100);
});
/**
* `parseInt` accepts garbage-suffixed numbers (`parseInt("123abc", 10) === 123`)
* and returns `NaN` for non-numeric input. Use this helper at loader boundaries
* for URL-supplied integer params so a malformed URL silently falls back to
* `undefined` rather than nudging downstream logic with a partial or NaN value.
*/
export function parseFiniteInt(value: string | null | undefined): number | undefined {
if (value == null || value === "") return undefined;
const n = Number.parseInt(value, 10);
return Number.isFinite(n) ? n : undefined;
}
export function objectToSearchParams(
obj:
| undefined
| Record<string, string | string[] | number | number[] | boolean | boolean[] | undefined>
): URLSearchParams | undefined {
if (!obj) return undefined;
const searchParams = new URLSearchParams();
//for each item add to the search params, skip undefined and join arrays with commas
Object.entries(obj).forEach(([key, value]) => {
if (value === undefined) return;
if (Array.isArray(value)) {
for (const v of value) {
searchParams.append(key, v.toString());
}
} else {
searchParams.append(key, value.toString());
}
});
return searchParams;
}
class SearchParams<TParams extends ParamType> {
constructor(
private params: TParams,
readonly schema: ZodType<TParams>
) {}
get(key: keyof TParams) {
return this.params[key];
}
getAll() {
return this.params;
}
set(key: keyof TParams, value: TParams[keyof TParams]) {
//check it matches the schema
const newParams = { ...this.params, [key]: value };
const result = parseSearchParams(newParams, this.schema);
if (!result.success) {
return { success: false, error: result.error };
}
this.params = newParams;
return { success: true };
}
}
type SearchParamsResult<TParams extends ParamType> =
| { success: true; params: SearchParams<TParams> }
| { success: false; error: string };
type ParamType = Record<string, any>;
export function createSearchParams<TParams extends ParamType>(
url: string,
schema: ZodType<TParams>
): SearchParamsResult<TParams> {
const searchParams = new URL(url).searchParams;
const params = Object.fromEntries(searchParams.entries());
const parsed = parseSearchParams(params, schema);
if (!parsed.success) {
return { success: false, error: parsed.error };
}
return { success: true, params: new SearchParams<TParams>(parsed.params as TParams, schema) };
}
function parseSearchParams<TParams>(params: unknown, schema: ZodType<TParams>) {
const parsedParams = schema.safeParse(params);
if (!parsedParams.success) {
const friendlyError = fromZodError(parsedParams.error, {
prefix: "There's an issue with your search params",
}).message;
return { success: false as const, error: friendlyError };
}
return { success: true as const, params: parsedParams.data };
}