-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathparsers.ts
259 lines (238 loc) · 8.02 KB
/
parsers.ts
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
import { z } from 'zod';
import { createErrorResponse } from './errors';
import type { DataFunctionArgs } from '@remix-run/server-runtime';
import type {
output,
SafeParseReturnType,
ZodObject,
ZodRawShape,
ZodTypeAny,
} from 'zod';
type Params = DataFunctionArgs['params'];
type Options<Parser = SearchParamsParser> = {
/** Custom error message for when the validation fails. */
message?: string;
/** Status code for thrown request when validation fails. */
status?: number;
/** Custom URLSearchParams parsing function. */
parser?: Parser;
};
/**
* Type assertion function avoids problems with some bundlers when
* using `instanceof` to check the type of a `schema` param.
*/
const isZodType = (input: ZodRawShape | ZodTypeAny): input is ZodTypeAny => {
return typeof input.parse === 'function';
}
/**
* Generic return type for parseX functions.
*/
type ParsedData<T extends ZodRawShape | ZodTypeAny> = T extends ZodTypeAny
? output<T>
: T extends ZodRawShape
? output<ZodObject<T>>
: never;
/**
* Generic return type for parseXSafe functions.
*/
type SafeParsedData<T extends ZodRawShape | ZodTypeAny> = T extends ZodTypeAny
? SafeParseReturnType<z.infer<T>, ParsedData<T>>
: T extends ZodRawShape
? SafeParseReturnType<ZodObject<T>, ParsedData<T>>
: never;
/**
* Parse and validate Params from LoaderArgs or ActionArgs. Throws an error if validation fails.
* @param params - A Remix Params object.
* @param schema - A Zod object shape or object schema to validate.
* @throws {Response} - Throws an error Response if validation fails.
*/
export function parseParams<T extends ZodRawShape | ZodTypeAny>(
params: Params,
schema: T,
options?: Options
): ParsedData<T> {
try {
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return finalSchema.parse(params);
} catch (error) {
throw createErrorResponse(options);
}
}
/**
* Parse and validate Params from LoaderArgs or ActionArgs. Doesn't throw if validation fails.
* @param params - A Remix Params object.
* @param schema - A Zod object shape or object schema to validate.
* @returns {SafeParseReturnType} - An object with the parsed data or a ZodError.
*/
export function parseParamsSafe<T extends ZodRawShape | ZodTypeAny>(
params: Params,
schema: T
): SafeParsedData<T> {
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return finalSchema.safeParse(params) as SafeParsedData<T>;
}
/**
* Parse and validate URLSearchParams or a Request. Throws an error if validation fails.
* @param request - A Request or URLSearchParams
* @param schema - A Zod object shape or object schema to validate.
* @throws {Response} - Throws an error Response if validation fails.
*/
export function parseQuery<T extends ZodRawShape | ZodTypeAny>(
request: Request | URLSearchParams,
schema: T,
options?: Options
): ParsedData<T> {
try {
const searchParams = isURLSearchParams(request)
? request
: getSearchParamsFromRequest(request);
const params = parseSearchParams(searchParams, options?.parser);
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return finalSchema.parse(params);
} catch (error) {
throw createErrorResponse(options);
}
}
/**
* Parse and validate URLSearchParams or a Request. Doesn't throw if validation fails.
* @param request - A Request or URLSearchParams
* @param schema - A Zod object shape or object schema to validate.
* @returns {SafeParseReturnType} - An object with the parsed data or a ZodError.
*/
export function parseQuerySafe<T extends ZodRawShape | ZodTypeAny>(
request: Request | URLSearchParams,
schema: T,
options?: Options
): SafeParsedData<T> {
const searchParams = isURLSearchParams(request)
? request
: getSearchParamsFromRequest(request);
const params = parseSearchParams(searchParams, options?.parser);
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return finalSchema.safeParse(params) as SafeParsedData<T>;
}
/**
* Parse and validate FormData from a Request. Throws an error if validation fails.
* @param request - A Request or FormData
* @param schema - A Zod object shape or object schema to validate.
* @throws {Response} - Throws an error Response if validation fails.
*/
export async function parseForm<
T extends ZodRawShape | ZodTypeAny,
Parser extends SearchParamsParser<any>
>(
request: Request | FormData,
schema: T,
options?: Options<Parser>
): Promise<ParsedData<T>> {
try {
const formData = isFormData(request)
? request
: await request.clone().formData();
const data = await parseFormData(formData, options?.parser);
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return await finalSchema.parseAsync(data);
} catch (error) {
throw createErrorResponse(options);
}
}
/**
* Parse and validate FormData from a Request. Doesn't throw if validation fails.
* @param request - A Request or FormData
* @param schema - A Zod object shape or object schema to validate.
* @returns {SafeParseReturnType} - An object with the parsed data or a ZodError.
*/
export async function parseFormSafe<
T extends ZodRawShape | ZodTypeAny,
Parser extends SearchParamsParser<any>
>(
request: Request | FormData,
schema: T,
options?: Options<Parser>
): Promise<SafeParsedData<T>> {
const formData = isFormData(request)
? request
: await request.clone().formData();
const data = await parseFormData(formData, options?.parser);
const finalSchema = isZodType(schema) ? schema : z.object(schema);
return finalSchema.safeParseAsync(data) as Promise<SafeParsedData<T>>;
}
/**
* The data returned from parsing a URLSearchParams object.
*/
type ParsedSearchParams = Record<string, string | string[]>;
/**
* Function signature to allow for custom URLSearchParams parsing.
*/
type SearchParamsParser<T = ParsedSearchParams> = (
searchParams: URLSearchParams
) => T;
/**
* Check if an object entry value is an instance of Object
*/
function isObjectEntry([, value]: [string, FormDataEntryValue]) {
return value instanceof Object;
}
/**
* Get the form data from a request as an object.
*/
function parseFormData(formData: FormData, customParser?: SearchParamsParser) {
const objectEntries = [...formData.entries()].filter(isObjectEntry);
objectEntries.forEach(([key, value]) => {
formData.set(key, JSON.stringify(value));
});
// Context on `as any` usage: /~https://github.com/microsoft/TypeScript/issues/30584
return parseSearchParams(new URLSearchParams(formData as any), customParser);
}
/**
* Get the URLSearchParams as an object.
*/
function parseSearchParams(
searchParams: URLSearchParams,
customParser?: SearchParamsParser
): ParsedSearchParams {
const parser = customParser || parseSearchParamsDefault;
return parser(searchParams);
}
/**
* The default parser for URLSearchParams.
* Get the search params as an object. Create arrays for duplicate keys.
*/
const parseSearchParamsDefault: SearchParamsParser = (searchParams) => {
const values: ParsedSearchParams = {};
for (const [key, value] of searchParams) {
const currentVal = values[key];
if (currentVal && Array.isArray(currentVal)) {
currentVal.push(value);
} else if (currentVal) {
values[key] = [currentVal, value];
} else {
values[key] = value;
}
}
return values;
};
/**
* Get the search params from a request.
*/
function getSearchParamsFromRequest(request: Request): URLSearchParams {
const url = new URL(request.url);
return url.searchParams;
}
/**
* Check if value is an instance of FormData.
* This is a workaround for `instanceof` to support multiple platforms.
*/
function isFormData(value: unknown): value is FormData {
return getObjectTypeName(value) === 'FormData';
}
/**
* Check if value is an instance of URLSearchParams.
* This is a workaround for `instanceof` to support multiple platforms.
*/
function isURLSearchParams(value: unknown): value is FormData {
return getObjectTypeName(value) === 'URLSearchParams';
}
function getObjectTypeName(value: unknown): string {
return toString.call(value).slice(8, -1);
}