mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-06 20:37:44 +00:00
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
import type { z } from "zod";
|
|
|
|
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
|
|
export type ZodObjectOrWrapped =
|
|
| z.ZodObject<any, any>
|
|
| z.ZodEffects<z.ZodObject<any, any>>;
|
|
|
|
/**
|
|
* Beautify a camelCase string.
|
|
* e.g. "myString" -> "My String"
|
|
*/
|
|
export function beautifyObjectName(string: string) {
|
|
// Remove bracketed indices
|
|
// if numbers only return the string
|
|
let output = string.replace(/\[\d+\]/g, "").replace(/([A-Z])/g, " $1");
|
|
output = output.charAt(0).toUpperCase() + output.slice(1);
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* Parse string and extract the index
|
|
* @param string
|
|
* @returns index or undefined
|
|
*/
|
|
export function getIndexIfArray(string: string) {
|
|
const indexRegex = /\[(\d+)\]/;
|
|
// Match the index
|
|
const match = string.match(indexRegex);
|
|
// Extract the index (number)
|
|
const index = match ? Number.parseInt(match[1]) : undefined;
|
|
return index;
|
|
}
|
|
|
|
/**
|
|
* Get the lowest level Zod type.
|
|
* This will unpack optionals, refinements, etc.
|
|
*/
|
|
export function getBaseSchema<
|
|
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
|
|
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType | null {
|
|
if (!schema) return null;
|
|
if ("innerType" in schema._def)
|
|
return getBaseSchema(schema._def.innerType as ChildType);
|
|
|
|
if ("schema" in schema._def)
|
|
return getBaseSchema(schema._def.schema as ChildType);
|
|
|
|
return schema as ChildType;
|
|
}
|
|
|
|
/**
|
|
* Get the type name of the lowest level Zod type.
|
|
* This will unpack optionals, refinements, etc.
|
|
*/
|
|
export function getBaseType(schema: z.ZodAny) {
|
|
const baseSchema = getBaseSchema(schema);
|
|
return baseSchema ? baseSchema._def.typeName : "";
|
|
}
|
|
|
|
/**
|
|
* Search for a "ZodDefault" in the Zod stack and return its value.
|
|
*/
|
|
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
|
|
const typedSchema = schema as unknown as z.ZodDefault<
|
|
z.ZodNumber | z.ZodString
|
|
>;
|
|
|
|
if (typedSchema._def.typeName === "ZodDefault")
|
|
return typedSchema._def.defaultValue();
|
|
|
|
if ("innerType" in typedSchema._def) {
|
|
return getDefaultValueInZodStack(
|
|
typedSchema._def.innerType as unknown as z.ZodAny,
|
|
);
|
|
}
|
|
if ("schema" in typedSchema._def) {
|
|
return getDefaultValueInZodStack(
|
|
(typedSchema._def as any).schema as z.ZodAny,
|
|
);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
export function getObjectFormSchema(
|
|
schema: ZodObjectOrWrapped,
|
|
): z.ZodObject<any, any> {
|
|
if (schema?._def.typeName === "ZodEffects") {
|
|
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
|
|
return getObjectFormSchema(typedSchema._def.schema);
|
|
}
|
|
return schema as z.ZodObject<any, any>;
|
|
}
|
|
|
|
function isIndex(value: unknown): value is number {
|
|
return Number(value) >= 0;
|
|
}
|
|
/**
|
|
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
|
|
*/
|
|
export function normalizeFormPath(path: string): string {
|
|
const pathArr = path.split(".");
|
|
if (!pathArr.length) return "";
|
|
|
|
let fullPath = String(pathArr[0]);
|
|
for (let i = 1; i < pathArr.length; i++) {
|
|
if (isIndex(pathArr[i])) {
|
|
fullPath += `[${pathArr[i]}]`;
|
|
continue;
|
|
}
|
|
|
|
fullPath += `.${pathArr[i]}`;
|
|
}
|
|
|
|
return fullPath;
|
|
}
|
|
|
|
type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord };
|
|
/**
|
|
* Checks if the path opted out of nested fields using `[fieldName]` syntax
|
|
*/
|
|
export function isNotNestedPath(path: string) {
|
|
return /^\[.+\]$/.test(path);
|
|
}
|
|
function isObject(obj: unknown): obj is Record<string, unknown> {
|
|
return (
|
|
obj !== null && !!obj && typeof obj === "object" && !Array.isArray(obj)
|
|
);
|
|
}
|
|
function isContainerValue(value: unknown): value is Record<string, unknown> {
|
|
return isObject(value) || Array.isArray(value);
|
|
}
|
|
function cleanupNonNestedPath(path: string) {
|
|
if (isNotNestedPath(path)) return path.replace(/\[|\]/g, "");
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Gets a nested property value from an object
|
|
*/
|
|
export function getFromPath<TValue = unknown>(
|
|
object: NestedRecord | undefined,
|
|
path: string,
|
|
): TValue | undefined;
|
|
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
|
object: NestedRecord | undefined,
|
|
path: string,
|
|
fallback?: TFallback,
|
|
): TValue | TFallback;
|
|
export function getFromPath<TValue = unknown, TFallback = TValue>(
|
|
object: NestedRecord | undefined,
|
|
path: string,
|
|
fallback?: TFallback,
|
|
): TValue | TFallback | undefined {
|
|
if (!object) return fallback;
|
|
|
|
if (isNotNestedPath(path))
|
|
return object[cleanupNonNestedPath(path)] as TValue | undefined;
|
|
|
|
const resolvedValue = (path || "")
|
|
.split(/\.|\[(\d+)\]/)
|
|
.filter(Boolean)
|
|
.reduce((acc, propKey) => {
|
|
if (isContainerValue(acc) && propKey in acc) return acc[propKey];
|
|
|
|
return fallback;
|
|
}, object as unknown);
|
|
|
|
return resolvedValue as TValue | undefined;
|
|
}
|