mirror of
https://github.com/better-auth/better-auth.git
synced 2026-07-17 17:01:18 -05:00
feat(core): add string case conversion utilities (#9727)
This commit is contained in:
5
.changeset/string-case-utils.md
Normal file
5
.changeset/string-case-utils.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/core": patch
|
||||
---
|
||||
|
||||
Add `toCamelCase`, `toSnakeCase`, `toPascalCase`, and `toKebabCase` to `@better-auth/core/utils/string`.
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
77
packages/core/src/utils/string.test.ts
Normal file
77
packages/core/src/utils/string.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user