feat(core): add string case conversion utilities (#9727)

This commit is contained in:
Taesu
2026-05-25 08:40:22 +09:00
committed by GitHub
parent 5aa5682e18
commit 83fa3695e7
6 changed files with 125 additions and 17 deletions

View File

@@ -0,0 +1,5 @@
---
"@better-auth/core": patch
---
Add `toCamelCase`, `toSnakeCase`, `toPascalCase`, and `toKebabCase` to `@better-auth/core/utils/string`.

View File

@@ -3,6 +3,7 @@ import type {
ClientAtomListener,
ClientFetchOption,
} from "@better-auth/core";
import { toKebabCase } from "@better-auth/core/utils/string";
import type { BetterFetch } from "@better-fetch/fetch";
import type { Atom } from "nanostores";
import { isAtom } from "../utils/is-atom";
@@ -67,13 +68,7 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
return createProxy(fullPath);
},
apply: async (_, __, args) => {
const routePath =
"/" +
path
.map((segment) =>
segment.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`),
)
.join("/");
const routePath = "/" + path.map(toKebabCase).join("/");
const arg = (args[0] || {}) as ProxyRequest;
const fetchOptions = (args[1] || {}) as ClientFetchOption;
const { query, fetchOptions: argFetchOptions, ...body } = arg;

View File

@@ -4,7 +4,7 @@ import type {
DBFieldAttributeConfig,
DBFieldType,
} from "@better-auth/core/db";
import { capitalizeFirstLetter } from "@better-auth/core/utils/string";
import { toPascalCase } from "@better-auth/core/utils/string";
import type {
Endpoint,
EndpointOptions,
@@ -427,7 +427,7 @@ export async function generator(ctx: AuthContext, options: BetterAuthOptions) {
) => {
if (!operationId) return undefined;
const base = seenOperationIds.has(operationId)
? `${operationId}${capitalizeFirstLetter(method.toLowerCase())}`
? `${operationId}${toPascalCase(method)}`
: operationId;
let result = base;
let n = 2;

View File

@@ -1,4 +1,5 @@
import { existsSync } from "node:fs";
import { toSnakeCase } from "@better-auth/core/utils/string";
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
import type { BetterAuthDBSchema, DBFieldAttribute } from "better-auth/db";
import { getAuthTables } from "better-auth/db";
@@ -7,14 +8,7 @@ import prettier from "prettier";
import type { SchemaGenerator } from "./types";
function convertToSnakeCase(str: string, camelCase?: boolean) {
if (camelCase) {
return str;
}
// Handle consecutive capitals (like ID, URL, API) by treating them as a single word
return str
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // Handle AABb -> AA_Bb
.replace(/([a-z\d])([A-Z])/g, "$1_$2") // Handle aBb -> a_Bb
.toLowerCase();
return camelCase ? str : toSnakeCase(str);
}
export const generateDrizzleSchema: SchemaGenerator = async ({

View File

@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import {
capitalizeFirstLetter,
toCamelCase,
toKebabCase,
toPascalCase,
toSnakeCase,
} from "./string";
describe("capitalizeFirstLetter", () => {
it("uppercases the first character only", () => {
expect(capitalizeFirstLetter("hello")).toBe("Hello");
expect(capitalizeFirstLetter("HELLO")).toBe("HELLO");
expect(capitalizeFirstLetter("")).toBe("");
});
});
describe("toSnakeCase", () => {
it.each([
["userId", "user_id"],
["user_id", "user_id"],
["UserId", "user_id"],
["USER_ID", "user_id"],
["URL", "url"],
["URLPath", "url_path"],
["my-kebab-case", "my_kebab_case"],
["foo123Bar", "foo123_bar"],
["", ""],
["it's a test", "its_a_test"],
["한글Test", "한글_test"],
["user_한글_id", "user_한글_id"],
["caféBar", "café_bar"],
])("%s -> %s", (input, expected) => {
expect(toSnakeCase(input)).toBe(expected);
});
});
describe("toKebabCase", () => {
it.each([
["userId", "user-id"],
["user_id", "user-id"],
["UserId", "user-id"],
["URLPath", "url-path"],
["", ""],
])("%s -> %s", (input, expected) => {
expect(toKebabCase(input)).toBe(expected);
});
});
describe("toCamelCase", () => {
it.each([
["user_id", "userId"],
["user-id", "userId"],
["UserId", "userId"],
["URL_PATH", "urlPATH"],
["my-kebab-case", "myKebabCase"],
["", ""],
])("%s -> %s", (input, expected) => {
expect(toCamelCase(input)).toBe(expected);
});
});
describe("toPascalCase", () => {
it.each([
["user_id", "UserId"],
["user-id", "UserId"],
["userId", "UserId"],
["URL_PATH", "UrlPath"],
["get", "Get"],
["POST", "Post"],
["my-kebab-case", "MyKebabCase"],
["한글test", "한글Test"],
["", ""],
])("%s -> %s", (input, expected) => {
expect(toPascalCase(input)).toBe(expected);
});
});

View File

@@ -1,3 +1,40 @@
export function capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const WORD_PATTERN =
/[\p{Ll}\d]+|\p{Lu}+(?!\p{Ll})|\p{Lu}[\p{Ll}\d]+|\p{Lo}+/gu;
const APOSTROPHE_PATTERN = /['\u2019]/g;
function splitWords(input: string): string[] {
return input.replace(APOSTROPHE_PATTERN, "").match(WORD_PATTERN) ?? [];
}
export function toSnakeCase(input: string): string {
return splitWords(input)
.map((word) => word.toLowerCase())
.join("_");
}
export function toKebabCase(input: string): string {
return splitWords(input)
.map((word) => word.toLowerCase())
.join("-");
}
export function toCamelCase(input: string): string {
return splitWords(input).reduce((acc, word, i) => {
return (
acc +
(i === 0
? word.toLowerCase()
: `${word[0]!.toUpperCase()}${word.slice(1)}`)
);
}, "");
}
export function toPascalCase(input: string): string {
return splitWords(input)
.map((word) => `${word[0]!.toUpperCase()}${word.slice(1).toLowerCase()}`)
.join("");
}