Skip to content

Commit 1b49085

Browse files
committed
fix: add notes length validation and consolidate character types
1 parent 83b5729 commit 1b49085

3 files changed

Lines changed: 45 additions & 17 deletions

File tree

apps/backend/src/lib/__tests__/validation.unit.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,34 @@ describe("Character Schemas", () => {
12131213
const result = createCharacterSchema.safeParse(invalidData);
12141214
expect(result.success).toBe(false);
12151215
});
1216+
1217+
it("should accept notes with exactly 10000 characters", () => {
1218+
const validData = {
1219+
projectId: "550e8400-e29b-41d4-a716-446655440000",
1220+
name: "Character Name",
1221+
displayName: "Display Name",
1222+
renpyTag: "char",
1223+
color: "#FF5733",
1224+
notes: "x".repeat(10000),
1225+
};
1226+
1227+
const result = createCharacterSchema.safeParse(validData);
1228+
expect(result.success).toBe(true);
1229+
});
1230+
1231+
it("should reject notes with 10001 characters", () => {
1232+
const validData = {
1233+
projectId: "550e8400-e29b-41d4-a716-446655440000",
1234+
name: "Character Name",
1235+
displayName: "Display Name",
1236+
renpyTag: "char",
1237+
color: "#FF5733",
1238+
notes: "x".repeat(10001),
1239+
};
1240+
1241+
const result = createCharacterSchema.safeParse(validData);
1242+
expect(result.success).toBe(false);
1243+
});
12161244
});
12171245

12181246
describe("characterIdParamsSchema", () => {

apps/backend/src/services/characters.service.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,7 @@ import type {
4949
// Types
5050
// ============================================================================
5151

52-
/** Character summary returned in list views */
53-
export interface CharacterSummary {
54-
id: string;
55-
name: string;
56-
displayName: string;
57-
renpyTag: string;
58-
color: string;
59-
routeAffiliation: string | null;
60-
isLoveInterest: boolean;
61-
isNarrator: boolean;
62-
notes: string | null;
63-
conditionalPrefix: string | null;
64-
avatarUrl: string | null;
65-
}
66-
67-
/** Full character detail returned for single-character operations */
52+
/** Character detail returned for list-views and single-character operations */
6853
export interface CharacterDetail {
6954
id: string;
7055
name: string;
@@ -472,7 +457,7 @@ export class CharactersService {
472457
async listCharacters(
473458
projectId: string,
474459
userId: string
475-
): Promise<CharacterSummary[]> {
460+
): Promise<CharacterDetail[]> {
476461
await requireProjectOwnership(projectId, userId);
477462

478463
const db = getDb();

apps/frontend/src/components/CharacterEditDialog.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ interface CharacterFormState {
5151
displayNameError?: string;
5252
renpyTagError?: string;
5353
colorError?: string;
54+
notesError?: string;
5455
}
5556

5657
type FormAction =
@@ -63,6 +64,7 @@ type FormAction =
6364
| { type: "SET_NAME_ERROR"; value: string }
6465
| { type: "SET_DISPLAY_NAME_ERROR"; value: string }
6566
| { type: "SET_RENPY_TAG_ERROR"; value: string }
67+
| { type: "SET_NOTES_ERROR"; value: string }
6668
| { type: "SET_COLOR_ERROR"; value: string };
6769

6870
const INITIAL_EMPTY: CharacterFormState = {
@@ -83,6 +85,7 @@ const INITIAL_EMPTY: CharacterFormState = {
8385
displayNameError: "",
8486
renpyTagError: "",
8587
colorError: "",
88+
notesError: "",
8689
};
8790

8891
function formReducer(
@@ -134,6 +137,8 @@ function formReducer(
134137
return { ...state, renpyTagError: action.value };
135138
case "SET_COLOR_ERROR":
136139
return { ...state, colorError: action.value };
140+
case "SET_NOTES_ERROR":
141+
return { ...state, notesError: action.value };
137142
}
138143
}
139144

@@ -158,6 +163,9 @@ function validateForm(state: CharacterFormState): {
158163
if (!/^#[0-9A-Fa-f]{6}$/.test(state.color)) {
159164
errors.color = "Color must be valid hex (#RRGGBB)";
160165
}
166+
if (state.notes.length > 10000) {
167+
errors.notes = "Notes must be 10000 characters or fewer";
168+
}
161169

162170
return { valid: Object.keys(errors).length === 0, errors };
163171
}
@@ -245,6 +253,7 @@ export function CharacterEditDialog({
245253
dispatch({ type: "SET_DISPLAY_NAME_ERROR", value: "" });
246254
dispatch({ type: "SET_RENPY_TAG_ERROR", value: "" });
247255
dispatch({ type: "SET_COLOR_ERROR", value: "" });
256+
dispatch({ type: "SET_NOTES_ERROR", value: "" });
248257
};
249258

250259
const handleAvatarSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -295,6 +304,8 @@ export function CharacterEditDialog({
295304
dispatch({ type: "SET_RENPY_TAG_ERROR", value: errors.renpyTag });
296305
if (errors.color)
297306
dispatch({ type: "SET_COLOR_ERROR", value: errors.color });
307+
if (errors.notes)
308+
dispatch({ type: "SET_NOTES_ERROR", value: errors.notes });
298309
return;
299310
}
300311

@@ -557,7 +568,11 @@ export function CharacterEditDialog({
557568
value={form.notes}
558569
onChange={(e) => handleFieldChange("notes", e.target.value)}
559570
disabled={isSaving}
571+
maxLength={10000}
560572
/>
573+
{form.notesError && (
574+
<p className="text-xs text-destructive">{form.notesError}</p>
575+
)}
561576
</div>
562577

563578
{/* Love Interest + Narrator */}

0 commit comments

Comments
 (0)