fix(#1): Address code review findings

- Convert ApiResponse to discriminated union for type-safe error handling
- Add HealthStatus type with HealthState literal union
- Make BaseEntity fields readonly for immutability
- Add GlobalExceptionFilter with structured logging
- Add port validation with clear error messages in main.ts
- Improve parseDate to log warnings for invalid dates
- Add comprehensive Button tests (variants, onClick, disabled)
- Add slugify edge case tests (empty, special chars, numbers)
- Create ESLint configs for all packages
- Remove compiled JS files from src directories
- Convert .prettierrc.js to .prettierrc.json

Refs #1

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-01-28 15:07:04 -06:00
parent f277afde36
commit 355cf2124b
33 changed files with 411 additions and 191 deletions

View File

@@ -1,38 +1,121 @@
/**
* Base entity type with common fields
* @invariant updatedAt >= createdAt
*/
export interface BaseEntity {
id: string;
createdAt: Date;
readonly id: string;
readonly createdAt: Date;
updatedAt: Date;
}
/**
* API response wrapper
* API response wrapper - discriminated union for type-safe error handling
*
* Usage:
* ```typescript
* if (response.success) {
* // TypeScript knows response.data exists
* console.log(response.data);
* } else {
* // TypeScript knows response.message exists
* console.error(response.message);
* }
* ```
*/
export interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
export type ApiResponse<T> =
| { success: true; data: T; message?: string }
| { success: false; data?: never; message: string; errorCode?: string };
/**
* Helper to create a successful API response
*/
export function successResponse<T>(data: T, message?: string): ApiResponse<T> {
if (message !== undefined) {
return { success: true, data, message };
}
return { success: true, data };
}
/**
* Helper to create an error API response
*/
export function errorResponse<T>(message: string, errorCode?: string): ApiResponse<T> {
if (errorCode !== undefined) {
return { success: false, message, errorCode };
}
return { success: false, message };
}
/**
* Pagination parameters
*/
export interface PaginationParams {
export interface PaginationParams<SortableFields extends string = string> {
/** Page number (1-indexed) */
page: number;
/** Items per page */
limit: number;
sortBy?: string;
/** Field to sort by */
sortBy?: SortableFields;
/** Sort direction */
sortOrder?: "asc" | "desc";
}
/**
* Paginated response
* @invariant totalPages === Math.ceil(total / limit)
* @invariant data.length <= limit
* @invariant page >= 1 && page <= totalPages (when total > 0)
*/
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
readonly data: readonly T[];
readonly total: number;
readonly page: number;
readonly limit: number;
readonly totalPages: number;
}
/**
* Helper to create a paginated response with calculated totalPages
*/
export function createPaginatedResponse<T>(
data: T[],
total: number,
page: number,
limit: number
): PaginatedResponse<T> {
if (limit <= 0) {
throw new Error("limit must be positive");
}
if (page < 1) {
throw new Error("page must be >= 1");
}
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit) || 1,
};
}
/**
* Health check status values
*/
export type HealthState = "healthy" | "degraded" | "unhealthy";
/**
* Health check response
*/
export interface HealthStatus {
status: HealthState;
timestamp: string;
version?: string;
checks?: Record<
string,
{
status: HealthState;
message?: string;
}
>;
}

View File

@@ -35,6 +35,38 @@ describe("slugify", () => {
it("should trim leading and trailing hyphens", () => {
expect(slugify(" Hello World ")).toBe("hello-world");
});
it("should handle empty string", () => {
expect(slugify("")).toBe("");
});
it("should handle string with only whitespace", () => {
expect(slugify(" ")).toBe("");
});
it("should handle consecutive special characters", () => {
expect(slugify("Hello---World")).toBe("hello-world");
expect(slugify("Hello___World")).toBe("hello-world");
expect(slugify("Hello World")).toBe("hello-world");
});
it("should handle numbers", () => {
expect(slugify("Product 123")).toBe("product-123");
expect(slugify("2024 New Year")).toBe("2024-new-year");
});
it("should handle mixed case", () => {
expect(slugify("HeLLo WoRLd")).toBe("hello-world");
});
it("should remove leading/trailing special chars", () => {
expect(slugify("---Hello World---")).toBe("hello-world");
expect(slugify("!@#Hello World!@#")).toBe("hello-world");
});
it("should handle string with only special characters", () => {
expect(slugify("!@#$%^&*()")).toBe("");
});
});
describe("sleep", () => {

View File

@@ -1,11 +1,16 @@
/**
* Safely parse a date string or return undefined
* Safely parse a date string or return undefined.
* Logs a warning if an invalid date string is passed.
*/
export function parseDate(value: string | Date | undefined | null): Date | undefined {
if (!value) return undefined;
if (value instanceof Date) return value;
const parsed = new Date(value);
return isNaN(parsed.getTime()) ? undefined : parsed;
if (isNaN(parsed.getTime())) {
console.warn(`parseDate: Invalid date string received: "${value}"`);
return undefined;
}
return parsed;
}
/**