Skip to content

Commit 62f537b

Browse files
borisno2claude
andcommitted
Implement proper case conversion for list names
Add comprehensive case utilities to handle naming conventions: - Config list names: PascalCase (AuthUser, BlogPost) - Prisma models: PascalCase (AuthUser, BlogPost) - Prisma client properties: camelCase (authUser, blogPost) - Context db properties: camelCase (authUser, blogPost) - URLs: kebab-case (auth-user, blog-post) This fixes the bug where multi-word list names like "AuthUser" would become "authuser" instead of "authUser" when accessing the database. Changes: - Add case-utils.ts with conversion utilities - Update context to use getDbKey() for camelCase conversion - Update UI components to use getDbKey() instead of toLowerCase() - Export case utilities from @opensaas/core 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e079af1 commit 62f537b

7 files changed

Lines changed: 98 additions & 16 deletions

File tree

packages/core/src/context/index.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ValidationError,
1717
} from "../hooks/index.js";
1818
import { processNestedOperations } from "./nested-operations.js";
19+
import { getDbKey } from "../lib/case-utils.js";
1920

2021
/**
2122
* Prisma-like client type
@@ -46,9 +47,9 @@ export async function getContext<TPrisma extends PrismaClientLike = any>(
4647

4748
// Create access-controlled operations for each list
4849
for (const [listName, listConfig] of Object.entries(config.lists)) {
49-
const lowerListName = listName.toLowerCase();
50+
const dbKey = getDbKey(listName);
5051

51-
db[lowerListName] = {
52+
db[dbKey] = {
5253
findUnique: createFindUnique(listName, listConfig, prisma, context, config),
5354
findMany: createFindMany(listName, listConfig, prisma, context, config),
5455
create: createCreate(listName, listConfig, prisma, context, config),
@@ -65,7 +66,7 @@ export async function getContext<TPrisma extends PrismaClientLike = any>(
6566
data?: Record<string, any>;
6667
id?: string;
6768
}) {
68-
const dbKey = props.listKey.toLowerCase();
69+
const dbKey = getDbKey(props.listKey);
6970

7071
if (props.action === "create") {
7172
return await db[dbKey].create({ data: props.data });
@@ -133,7 +134,7 @@ function createFindUnique<TPrisma extends PrismaClientLike>(
133134
const include = args.include || accessControlledInclude;
134135

135136
// Execute query with optimized includes
136-
const model = (prisma as any)[listName.toLowerCase()];
137+
const model = (prisma as any)[getDbKey(listName)];
137138
const item = await model.findFirst({
138139
where,
139140
include,
@@ -205,7 +206,7 @@ function createFindMany<TPrisma extends PrismaClientLike>(
205206
const include = args?.include || accessControlledInclude;
206207

207208
// Execute query with optimized includes
208-
const model = (prisma as any)[listName.toLowerCase()];
209+
const model = (prisma as any)[getDbKey(listName)];
209210
const items = await model.findMany({
210211
where,
211212
take: args?.take,
@@ -305,7 +306,7 @@ function createCreate<TPrisma extends PrismaClientLike>(
305306
});
306307

307308
// 7. Execute database create
308-
const model = (prisma as any)[listName.toLowerCase()];
309+
const model = (prisma as any)[getDbKey(listName)];
309310
const item = await model.create({
310311
data,
311312
});
@@ -344,7 +345,7 @@ function createUpdate<TPrisma extends PrismaClientLike>(
344345
) {
345346
return async (args: { where: { id: string }; data: any }) => {
346347
// 1. Fetch the item to pass to access control and hooks
347-
const model = (prisma as any)[listName.toLowerCase()];
348+
const model = (prisma as any)[getDbKey(listName)];
348349
const item = await model.findUnique({
349350
where: args.where,
350351
});
@@ -470,7 +471,7 @@ function createDelete<TPrisma extends PrismaClientLike>(
470471
) {
471472
return async (args: { where: { id: string } }) => {
472473
// 1. Fetch the item to pass to access control and hooks
473-
const model = (prisma as any)[listName.toLowerCase()];
474+
const model = (prisma as any)[getDbKey(listName)];
474475
const item = await model.findUnique({
475476
where: args.where,
476477
});
@@ -554,7 +555,7 @@ function createCount<TPrisma extends PrismaClientLike>(
554555
}
555556

556557
// Execute count
557-
const model = (prisma as any)[listName.toLowerCase()];
558+
const model = (prisma as any)[getDbKey(listName)];
558559
const count = await model.count({
559560
where,
560561
});

packages/core/src/context/nested-operations.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
validateFieldRules,
1212
ValidationError,
1313
} from "../hooks/index.js";
14+
import { getDbKey } from "../lib/case-utils.js";
1415

1516
/**
1617
* Check if a field config is a relationship field
@@ -108,7 +109,7 @@ async function processNestedConnect(
108109

109110
// Check update access for each item being connected
110111
for (const connection of connectionsArray) {
111-
const model = prisma[relatedListName.toLowerCase()];
112+
const model = prisma[getDbKey(relatedListName)];
112113

113114
// Fetch the item to check access
114115
const item = await model.findUnique({
@@ -165,7 +166,7 @@ async function processNestedUpdate(
165166

166167
const processedUpdates = await Promise.all(
167168
updatesArray.map(async (update) => {
168-
const model = prisma[relatedListName.toLowerCase()];
169+
const model = prisma[getDbKey(relatedListName)];
169170

170171
// Fetch the existing item
171172
const item = await model.findUnique({
@@ -270,7 +271,7 @@ async function processNestedConnectOrCreate(
270271

271272
// Check access for the connect portion (try to find existing item)
272273
try {
273-
const model = prisma[relatedListName.toLowerCase()];
274+
const model = prisma[getDbKey(relatedListName)];
274275
const existingItem = await model.findUnique({
275276
where: op.where,
276277
});

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export type {
3131
export { getContext } from "./context/index.js";
3232
export type { PrismaClientLike } from "./context/index.js";
3333

34+
// Utilities
35+
export { getDbKey, getUrlKey, getListKeyFromUrl, pascalToCamel, pascalToKebab, kebabToPascal, kebabToCamel } from "./lib/case-utils.js";
36+
3437
// Hooks
3538
export { ValidationError } from "./hooks/index.js";
3639

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Case conversion utilities for consistent naming across the framework
3+
*
4+
* - Config list names: PascalCase (e.g., "AuthUser", "BlogPost")
5+
* - Prisma models: PascalCase (e.g., "AuthUser", "BlogPost")
6+
* - Prisma client properties: camelCase (e.g., "authUser", "blogPost")
7+
* - Context db properties: camelCase (e.g., "authUser", "blogPost")
8+
* - URLs: kebab-case (e.g., "auth-user", "blog-post")
9+
*/
10+
11+
/**
12+
* Convert PascalCase to camelCase
13+
* AuthUser -> authUser
14+
* BlogPost -> blogPost
15+
*/
16+
export function pascalToCamel(str: string): string {
17+
return str.charAt(0).toLowerCase() + str.slice(1);
18+
}
19+
20+
/**
21+
* Convert PascalCase to kebab-case
22+
* AuthUser -> auth-user
23+
* BlogPost -> blog-post
24+
*/
25+
export function pascalToKebab(str: string): string {
26+
return str.replace(/([A-Z])/g, (match, p1, offset) => {
27+
return offset > 0 ? `-${p1.toLowerCase()}` : p1.toLowerCase();
28+
});
29+
}
30+
31+
/**
32+
* Convert kebab-case to PascalCase
33+
* auth-user -> AuthUser
34+
* blog-post -> BlogPost
35+
*/
36+
export function kebabToPascal(str: string): string {
37+
return str
38+
.split('-')
39+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
40+
.join('');
41+
}
42+
43+
/**
44+
* Convert kebab-case to camelCase
45+
* auth-user -> authUser
46+
* blog-post -> blogPost
47+
*/
48+
export function kebabToCamel(str: string): string {
49+
return str.replace(/-([a-z])/g, (match, p1) => p1.toUpperCase());
50+
}
51+
52+
/**
53+
* Get the database key for a list (camelCase)
54+
* Used for accessing context.db and prisma client
55+
*/
56+
export function getDbKey(listKey: string): string {
57+
return pascalToCamel(listKey);
58+
}
59+
60+
/**
61+
* Get the URL segment for a list (kebab-case)
62+
* Used for constructing admin URLs
63+
*/
64+
export function getUrlKey(listKey: string): string {
65+
return pascalToKebab(listKey);
66+
}
67+
68+
/**
69+
* Get the list key from a URL segment (PascalCase)
70+
* Used for parsing admin URLs
71+
*/
72+
export function getListKeyFromUrl(urlSegment: string): string {
73+
return kebabToPascal(urlSegment);
74+
}

packages/ui/src/components/Dashboard.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from "next/link";
22
import { formatListName } from "../lib/utils.js";
33
import type { AdminContext } from "../server/types.js";
4+
import { getDbKey } from "@opensaas/core";
45

56
export interface DashboardProps {
67
context: AdminContext;
@@ -22,7 +23,7 @@ export async function Dashboard({
2223
lists.map(async (listKey) => {
2324
try {
2425
const dbContext = context.context as any;
25-
const count = await dbContext.db[listKey.toLowerCase()]?.count();
26+
const count = await dbContext.db[getDbKey(listKey)]?.count();
2627
return { listKey, count: count || 0 };
2728
} catch (error) {
2829
console.error(`Failed to get count for ${listKey}:`, error);

packages/ui/src/components/ItemForm.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22
import { ItemFormClient } from "./ItemFormClient.js";
33
import { formatListName } from "../lib/utils.js";
44
import type { AdminContext, ServerActionInput } from "../server/types.js";
5+
import { getDbKey } from "@opensaas/core";
56

67
export interface ItemFormProps {
78
context: AdminContext;
@@ -43,7 +44,7 @@ export async function ItemForm({
4344
if (mode === "edit" && itemId) {
4445
try {
4546
const dbContext = context.context as any;
46-
itemData = await dbContext.db[listKey.toLowerCase()].findUnique({
47+
itemData = await dbContext.db[getDbKey(listKey)].findUnique({
4748
where: { id: itemId },
4849
});
4950

@@ -92,7 +93,7 @@ export async function ItemForm({
9293
if (relatedListConfig) {
9394
try {
9495
const dbContext = context.context as any;
95-
const relatedItems = await dbContext.db[relatedListName.toLowerCase()].findMany({});
96+
const relatedItems = await dbContext.db[getDbKey(relatedListName)].findMany({});
9697

9798
// Use 'name' field as label if it exists, otherwise use 'id'
9899
relationshipData[fieldName] = relatedItems.map((item: any) => ({

packages/ui/src/components/ListView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Link from "next/link";
22
import { ListViewClient } from "./ListViewClient.js";
33
import { formatListName } from "../lib/utils.js";
44
import type { AdminContext } from "../server/types.js";
5+
import { getDbKey } from "@opensaas/core";
56

67
export interface ListViewProps {
78
context: AdminContext;
@@ -24,7 +25,7 @@ export async function ListView({
2425
page = 1,
2526
pageSize = 50,
2627
}: ListViewProps) {
27-
const key = listKey.toLowerCase();
28+
const key = getDbKey(listKey);
2829
const listConfig = context.config.lists[listKey];
2930

3031
if (!listConfig) {

0 commit comments

Comments
 (0)