remix-params-helper
Version:
This package makes it simple to use Zod with standard URLSearchParams and FormData which are typically used in Remix apps.
219 lines (218 loc) • 6.18 kB
JavaScript
import {
ZodArray,
ZodBoolean,
ZodDate,
ZodDefault,
ZodEffects,
ZodEnum,
ZodLiteral,
ZodNativeEnum,
ZodNumber,
ZodObject,
ZodOptional,
ZodString
} from "zod";
function isIterable(maybeIterable) {
return Symbol.iterator in Object(maybeIterable);
}
function parseParams(o, schema, key, value) {
let shape = schema;
while (shape instanceof ZodObject || shape instanceof ZodEffects) {
shape = shape instanceof ZodObject ? shape.shape : shape instanceof ZodEffects ? shape._def.schema : null;
if (shape === null) {
throw new Error(`Could not find shape for key ${key}`);
}
}
if (key.includes(".")) {
let [parentProp, ...rest] = key.split(".");
o[parentProp] = o[parentProp] ?? {};
parseParams(o[parentProp], shape[parentProp], rest.join("."), value);
return;
}
let isArray = false;
if (key.includes("[]")) {
isArray = true;
key = key.replace("[]", "");
}
const def = shape[key];
if (def) {
processDef(def, o, key, value);
}
}
function getParamsInternal(params, schema) {
let o = {};
let entries = [];
if (isIterable(params)) {
entries = Array.from(params);
} else {
entries = Object.entries(params);
}
for (let [key, value] of entries) {
if (value === "") {
continue;
}
parseParams(o, schema, key, value);
}
const result = schema.safeParse(o);
if (result.success) {
return { success: true, data: result.data, errors: void 0 };
} else {
let errors = {};
const addError = (key, message) => {
if (!errors.hasOwnProperty(key)) {
errors[key] = message;
} else {
if (!Array.isArray(errors[key])) {
errors[key] = [errors[key]];
}
errors[key].push(message);
}
};
for (let issue of result.error.issues) {
const { message, path, code, expected, received } = issue;
const [key, index] = path;
let value = o[key];
let prop = key;
if (index !== void 0) {
value = value[index];
prop = `${key}[${index}]`;
}
addError(key, message);
}
return { success: false, data: void 0, errors };
}
}
function getParams(params, schema) {
return getParamsInternal(params, schema);
}
function getSearchParams(request, schema) {
let url = new URL(request.url);
return getParamsInternal(url.searchParams, schema);
}
async function getFormData(request, schema) {
let data = await request.formData();
return getParamsInternal(data, schema);
}
function getParamsOrFail(params, schema) {
const result = getParamsInternal(params, schema);
if (!result.success) {
throw new Error(JSON.stringify(result.errors));
}
return result.data;
}
function getSearchParamsOrFail(request, schema) {
let url = new URL(request.url);
const result = getParamsInternal(url.searchParams, schema);
if (!result.success) {
throw new Error(JSON.stringify(result.errors));
}
return result.data;
}
async function getFormDataOrFail(request, schema) {
let data = await request.formData();
const result = getParamsInternal(data, schema);
if (!result.success) {
throw new Error(JSON.stringify(result.errors));
}
return result.data;
}
function useFormInputProps(schema, options = {}) {
const shape = schema.shape;
const defaultOptions = options;
return function props(key, options2 = {}) {
options2 = { ...defaultOptions, ...options2 };
const def = shape[key];
if (!def) {
throw new Error(`no such key: ${key}`);
}
return getInputProps(key, def);
};
}
function processDef(def, o, key, value) {
let parsedValue;
if (def instanceof ZodString || def instanceof ZodLiteral) {
parsedValue = value;
} else if (def instanceof ZodNumber) {
const num = Number(value);
parsedValue = isNaN(num) ? value : num;
} else if (def instanceof ZodDate) {
const date = Date.parse(value);
parsedValue = isNaN(date) ? value : new Date(date);
} else if (def instanceof ZodBoolean) {
parsedValue = value === "true" ? true : value === "false" ? false : Boolean(value);
} else if (def instanceof ZodNativeEnum || def instanceof ZodEnum) {
parsedValue = value;
} else if (def instanceof ZodOptional || def instanceof ZodDefault) {
processDef(def._def.innerType, o, key, value);
return;
} else if (def instanceof ZodArray) {
if (o[key] === void 0) {
o[key] = [];
}
processDef(def.element, o, key, value);
return;
} else if (def instanceof ZodEffects) {
processDef(def._def.schema, o, key, value);
return;
} else {
throw new Error(`Unexpected type ${def._def.typeName} for key ${key}`);
}
if (Array.isArray(o[key])) {
o[key].push(parsedValue);
} else {
o[key] = parsedValue;
}
}
function getInputProps(name, def) {
let type = "text";
let min, max, minlength, maxlength, pattern;
if (def instanceof ZodString) {
if (def.isEmail) {
type = "email";
} else if (def.isURL) {
type = "url";
}
minlength = def.minLength ?? void 0;
maxlength = def.maxLength ?? void 0;
const check = def._def.checks.find((c) => c.kind === "regex");
pattern = check ? check.regex.source : void 0;
} else if (def instanceof ZodNumber) {
type = "number";
min = def.minValue ?? void 0;
max = def.maxValue ?? void 0;
} else if (def instanceof ZodBoolean) {
type = "checkbox";
} else if (def instanceof ZodDate) {
type = "date";
} else if (def instanceof ZodArray) {
return getInputProps(name, def.element);
} else if (def instanceof ZodOptional) {
return getInputProps(name, def.unwrap());
}
let inputProps = {
name,
type
};
if (!def.isOptional())
inputProps.required = true;
if (min)
inputProps.min = min;
if (max)
inputProps.max = max;
if (minlength && Number.isFinite(minlength))
inputProps.minLength = minlength;
if (maxlength && Number.isFinite(maxlength))
inputProps.maxLength = maxlength;
if (pattern)
inputProps.pattern = pattern;
return inputProps;
}
export {
getFormData,
getFormDataOrFail,
getParams,
getParamsOrFail,
getSearchParams,
getSearchParamsOrFail,
useFormInputProps
};