Files
2026-07-13 13:32:57 +08:00

119 lines
3.4 KiB
TypeScript

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 extends ParamType>(params: TParams, 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 };
}