forked from CSGOWTF/csgowtf
feat: complete Phase 3 - Domain Modeling & Data Layer
Implements comprehensive type system, runtime validation, API client, and testing infrastructure for CS2.WTF. TypeScript Interfaces (src/lib/types/): - Match.ts: Match, MatchPlayer, MatchListItem types - Player.ts: Player, PlayerMeta, PlayerProfile types - RoundStats.ts: Round economy and performance data - Weapon.ts: Weapon statistics with hit groups (HitGroup, WeaponType enums) - Message.ts: Chat messages with filtering support - api.ts: API responses, errors, and APIException class - Complete type safety with strict null checks Zod Schemas (src/lib/schemas/): - Runtime validation for all data models - Schema parsers with safe/unsafe variants - Special handling for backend typo (looses → losses) - Share code validation regex - CS2-specific validations (rank 0-30000, MR12 rounds) API Client (src/lib/api/): - client.ts: Axios-based HTTP client with error handling - Request cancellation support (AbortController) - Automatic retry logic for transient failures - Timeout handling (10s default) - Typed APIException errors - players.ts: Player endpoints (profile, meta, track/untrack, search) - matches.ts: Match endpoints (parse, details, weapons, rounds, chat, search) - Zod validation on all API responses MSW Mock Handlers (src/mocks/): - fixtures.ts: Comprehensive mock data for testing - handlers/players.ts: Mock player API endpoints - handlers/matches.ts: Mock match API endpoints - browser.ts: Browser MSW worker for development - server.ts: Node MSW server for Vitest tests - Realistic responses with delays and pagination - Safe integer IDs to avoid precision loss Configuration: - .env.example: Complete environment variable documentation - src/vite-env.d.ts: Vite environment type definitions - All strict TypeScript checks passing (0 errors, 0 warnings) Features: - Cancellable requests for search (prevent race conditions) - Data normalization (backend typo handling) - Comprehensive error types (NetworkError, Timeout, etc.) - Share code parsing and validation - Pagination support for players and matches - Mock data for offline development and testing Build Status: ✓ Production build successful Type Check: ✓ 0 errors, 0 warnings Lint: ✓ All checks passed Phase 3 Completion: 100% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
88
src/lib/schemas/player.schema.ts
Normal file
88
src/lib/schemas/player.schema.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { z } from 'zod';
|
||||
import { matchSchema, matchPlayerSchema } from './match.schema';
|
||||
|
||||
/**
|
||||
* Zod schemas for Player data models
|
||||
*/
|
||||
|
||||
/** Player schema */
|
||||
export const playerSchema = z.object({
|
||||
id: z.number().positive(),
|
||||
name: z.string().min(1),
|
||||
avatar: z.string().url(),
|
||||
vanity_url: z.string().optional(),
|
||||
vanity_url_real: z.string().optional(),
|
||||
steam_updated: z.string().datetime(),
|
||||
profile_created: z.string().datetime().optional(),
|
||||
wins: z.number().int().nonnegative().optional(),
|
||||
losses: z.number().int().nonnegative().optional(),
|
||||
// Also support backend's typo "looses"
|
||||
looses: z.number().int().nonnegative().optional(),
|
||||
ties: z.number().int().nonnegative().optional(),
|
||||
vac_count: z.number().int().nonnegative().optional(),
|
||||
vac_date: z.string().datetime().nullable().optional(),
|
||||
game_ban_count: z.number().int().nonnegative().optional(),
|
||||
game_ban_date: z.string().datetime().nullable().optional(),
|
||||
oldest_sharecode_seen: z.string().optional(),
|
||||
matches: z
|
||||
.array(
|
||||
matchSchema.extend({
|
||||
stats: matchPlayerSchema
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
/** Transform player data to normalize "looses" to "losses" */
|
||||
export const normalizePlayerData = (data: z.infer<typeof playerSchema>) => {
|
||||
if (data.looses !== undefined && data.losses === undefined) {
|
||||
return { ...data, losses: data.looses };
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
/** PlayerMeta schema */
|
||||
export const playerMetaSchema = z.object({
|
||||
id: z.number().positive(),
|
||||
name: z.string().min(1),
|
||||
avatar: z.string().url(),
|
||||
recent_matches: z.number().int().nonnegative(),
|
||||
last_match_date: z.string().datetime(),
|
||||
avg_kills: z.number().nonnegative(),
|
||||
avg_deaths: z.number().nonnegative(),
|
||||
avg_kast: z.number().nonnegative(),
|
||||
win_rate: z.number().nonnegative()
|
||||
});
|
||||
|
||||
/** PlayerProfile schema (extended with calculated stats) */
|
||||
export const playerProfileSchema = playerSchema.extend({
|
||||
total_matches: z.number().int().nonnegative(),
|
||||
kd_ratio: z.number().nonnegative(),
|
||||
win_rate: z.number().nonnegative(),
|
||||
avg_headshot_pct: z.number().nonnegative(),
|
||||
avg_kast: z.number().nonnegative(),
|
||||
current_rating: z.number().int().min(0).max(30000).optional(),
|
||||
peak_rating: z.number().int().min(0).max(30000).optional()
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
export const parsePlayer = (data: unknown) => {
|
||||
const parsed = playerSchema.parse(data);
|
||||
return normalizePlayerData(parsed);
|
||||
};
|
||||
|
||||
export const parsePlayerSafe = (data: unknown) => {
|
||||
const result = playerSchema.safeParse(data);
|
||||
if (result.success) {
|
||||
return { ...result, data: normalizePlayerData(result.data) };
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const parsePlayerMeta = (data: unknown) => playerMetaSchema.parse(data);
|
||||
export const parsePlayerProfile = (data: unknown) => playerProfileSchema.parse(data);
|
||||
|
||||
/** Infer TypeScript types */
|
||||
export type PlayerSchema = z.infer<typeof playerSchema>;
|
||||
export type PlayerMetaSchema = z.infer<typeof playerMetaSchema>;
|
||||
export type PlayerProfileSchema = z.infer<typeof playerProfileSchema>;
|
||||
Reference in New Issue
Block a user