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:
79
src/lib/schemas/api.schema.ts
Normal file
79
src/lib/schemas/api.schema.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { z } from 'zod';
|
||||
import { matchListItemSchema } from './match.schema';
|
||||
|
||||
/**
|
||||
* Zod schemas for API responses and error handling
|
||||
*/
|
||||
|
||||
/** APIError schema */
|
||||
export const apiErrorSchema = z.object({
|
||||
error: z.string(),
|
||||
message: z.string(),
|
||||
status_code: z.number().int(),
|
||||
timestamp: z.string().datetime().optional()
|
||||
});
|
||||
|
||||
/** Generic APIResponse schema */
|
||||
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
||||
z.object({
|
||||
data: dataSchema,
|
||||
success: z.boolean(),
|
||||
error: apiErrorSchema.optional()
|
||||
});
|
||||
|
||||
/** MatchParseResponse schema */
|
||||
export const matchParseResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
status: z.enum(['parsing', 'queued', 'completed', 'error']),
|
||||
message: z.string(),
|
||||
estimated_time: z.number().int().positive().optional()
|
||||
});
|
||||
|
||||
/** MatchParseStatus schema */
|
||||
export const matchParseStatusSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
status: z.enum(['pending', 'parsing', 'completed', 'error']),
|
||||
progress: z.number().int().min(0).max(100).optional(),
|
||||
error_message: z.string().optional()
|
||||
});
|
||||
|
||||
/** MatchesListResponse schema */
|
||||
export const matchesListResponseSchema = z.object({
|
||||
matches: z.array(matchListItemSchema),
|
||||
next_page_time: z.number().int().optional(),
|
||||
has_more: z.boolean(),
|
||||
total_count: z.number().int().nonnegative().optional()
|
||||
});
|
||||
|
||||
/** MatchesQueryParams schema */
|
||||
export const matchesQueryParamsSchema = z.object({
|
||||
limit: z.number().int().min(1).max(100).optional(),
|
||||
map: z.string().optional(),
|
||||
player_id: z.number().positive().optional(),
|
||||
before_time: z.number().int().positive().optional()
|
||||
});
|
||||
|
||||
/** TrackPlayerResponse schema */
|
||||
export const trackPlayerResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string()
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
export const parseAPIError = (data: unknown) => apiErrorSchema.parse(data);
|
||||
export const parseMatchParseResponse = (data: unknown) => matchParseResponseSchema.parse(data);
|
||||
export const parseMatchesList = (data: unknown) => matchesListResponseSchema.parse(data);
|
||||
export const parseMatchesQueryParams = (data: unknown) => matchesQueryParamsSchema.parse(data);
|
||||
export const parseTrackPlayerResponse = (data: unknown) => trackPlayerResponseSchema.parse(data);
|
||||
|
||||
/** Safe parser functions */
|
||||
export const parseMatchesListSafe = (data: unknown) => matchesListResponseSchema.safeParse(data);
|
||||
export const parseAPIErrorSafe = (data: unknown) => apiErrorSchema.safeParse(data);
|
||||
|
||||
/** Infer TypeScript types */
|
||||
export type APIErrorSchema = z.infer<typeof apiErrorSchema>;
|
||||
export type MatchParseResponseSchema = z.infer<typeof matchParseResponseSchema>;
|
||||
export type MatchParseStatusSchema = z.infer<typeof matchParseStatusSchema>;
|
||||
export type MatchesListResponseSchema = z.infer<typeof matchesListResponseSchema>;
|
||||
export type MatchesQueryParamsSchema = z.infer<typeof matchesQueryParamsSchema>;
|
||||
export type TrackPlayerResponseSchema = z.infer<typeof trackPlayerResponseSchema>;
|
||||
116
src/lib/schemas/index.ts
Normal file
116
src/lib/schemas/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Central export for all Zod schemas
|
||||
* Provides runtime validation for CS2.WTF data models
|
||||
*/
|
||||
|
||||
// Match schemas
|
||||
export {
|
||||
matchSchema,
|
||||
matchPlayerSchema,
|
||||
matchListItemSchema,
|
||||
parseMatch,
|
||||
parseMatchSafe,
|
||||
parseMatchPlayer,
|
||||
parseMatchListItem,
|
||||
type MatchSchema,
|
||||
type MatchPlayerSchema,
|
||||
type MatchListItemSchema
|
||||
} from './match.schema';
|
||||
|
||||
// Player schemas
|
||||
export {
|
||||
playerSchema,
|
||||
playerMetaSchema,
|
||||
playerProfileSchema,
|
||||
parsePlayer,
|
||||
parsePlayerSafe,
|
||||
parsePlayerMeta,
|
||||
parsePlayerProfile,
|
||||
normalizePlayerData,
|
||||
type PlayerSchema,
|
||||
type PlayerMetaSchema,
|
||||
type PlayerProfileSchema
|
||||
} from './player.schema';
|
||||
|
||||
// Round statistics schemas
|
||||
export {
|
||||
roundStatsSchema,
|
||||
roundDetailSchema,
|
||||
matchRoundsResponseSchema,
|
||||
teamRoundStatsSchema,
|
||||
parseRoundStats,
|
||||
parseRoundDetail,
|
||||
parseMatchRounds,
|
||||
parseTeamRoundStats,
|
||||
parseRoundStatsSafe,
|
||||
parseMatchRoundsSafe,
|
||||
type RoundStatsSchema,
|
||||
type RoundDetailSchema,
|
||||
type MatchRoundsResponseSchema,
|
||||
type TeamRoundStatsSchema
|
||||
} from './roundStats.schema';
|
||||
|
||||
// Weapon schemas
|
||||
export {
|
||||
weaponSchema,
|
||||
hitGroupsSchema,
|
||||
weaponStatsSchema,
|
||||
playerWeaponStatsSchema,
|
||||
matchWeaponsResponseSchema,
|
||||
parseWeapon,
|
||||
parseWeaponStats,
|
||||
parsePlayerWeaponStats,
|
||||
parseMatchWeapons,
|
||||
parseWeaponSafe,
|
||||
parseMatchWeaponsSafe,
|
||||
type WeaponSchema,
|
||||
type HitGroupsSchema,
|
||||
type WeaponStatsSchema,
|
||||
type PlayerWeaponStatsSchema,
|
||||
type MatchWeaponsResponseSchema
|
||||
} from './weapon.schema';
|
||||
|
||||
// Message/Chat schemas
|
||||
export {
|
||||
messageSchema,
|
||||
matchChatResponseSchema,
|
||||
enrichedMessageSchema,
|
||||
chatFilterSchema,
|
||||
chatStatsSchema,
|
||||
parseMessage,
|
||||
parseMatchChat,
|
||||
parseEnrichedMessage,
|
||||
parseChatFilter,
|
||||
parseChatStats,
|
||||
parseMessageSafe,
|
||||
parseMatchChatSafe,
|
||||
type MessageSchema,
|
||||
type MatchChatResponseSchema,
|
||||
type EnrichedMessageSchema,
|
||||
type ChatFilterSchema,
|
||||
type ChatStatsSchema
|
||||
} from './message.schema';
|
||||
|
||||
// API schemas
|
||||
export {
|
||||
apiErrorSchema,
|
||||
apiResponseSchema,
|
||||
matchParseResponseSchema,
|
||||
matchParseStatusSchema,
|
||||
matchesListResponseSchema,
|
||||
matchesQueryParamsSchema,
|
||||
trackPlayerResponseSchema,
|
||||
parseAPIError,
|
||||
parseMatchParseResponse,
|
||||
parseMatchesList,
|
||||
parseMatchesQueryParams,
|
||||
parseTrackPlayerResponse,
|
||||
parseMatchesListSafe,
|
||||
parseAPIErrorSafe,
|
||||
type APIErrorSchema,
|
||||
type MatchParseResponseSchema,
|
||||
type MatchParseStatusSchema,
|
||||
type MatchesListResponseSchema,
|
||||
type MatchesQueryParamsSchema,
|
||||
type TrackPlayerResponseSchema
|
||||
} from './api.schema';
|
||||
101
src/lib/schemas/match.schema.ts
Normal file
101
src/lib/schemas/match.schema.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schemas for Match data models
|
||||
* Provides runtime validation and type safety
|
||||
*/
|
||||
|
||||
/** MatchPlayer schema */
|
||||
export const matchPlayerSchema = z.object({
|
||||
id: z.number().positive(),
|
||||
name: z.string().min(1),
|
||||
avatar: z.string().url(),
|
||||
team_id: z.number().int().min(2).max(3), // 2 = T, 3 = CT
|
||||
|
||||
// Performance metrics
|
||||
kills: z.number().int().nonnegative(),
|
||||
deaths: z.number().int().nonnegative(),
|
||||
assists: z.number().int().nonnegative(),
|
||||
headshot: z.number().int().nonnegative(),
|
||||
mvp: z.number().int().nonnegative(),
|
||||
score: z.number().int().nonnegative(),
|
||||
kast: z.number().int().min(0).max(100),
|
||||
|
||||
// Rank (CS2 Premier rating: 0-30000)
|
||||
rank_old: z.number().int().min(0).max(30000).optional(),
|
||||
rank_new: z.number().int().min(0).max(30000).optional(),
|
||||
|
||||
// Damage
|
||||
dmg_enemy: z.number().int().nonnegative().optional(),
|
||||
dmg_team: z.number().int().nonnegative().optional(),
|
||||
|
||||
// Multi-kills
|
||||
mk_2: z.number().int().nonnegative().optional(),
|
||||
mk_3: z.number().int().nonnegative().optional(),
|
||||
mk_4: z.number().int().nonnegative().optional(),
|
||||
mk_5: z.number().int().nonnegative().optional(),
|
||||
|
||||
// Utility damage
|
||||
ud_he: z.number().int().nonnegative().optional(),
|
||||
ud_flames: z.number().int().nonnegative().optional(),
|
||||
ud_flash: z.number().int().nonnegative().optional(),
|
||||
ud_smoke: z.number().int().nonnegative().optional(),
|
||||
ud_decoy: z.number().int().nonnegative().optional(),
|
||||
|
||||
// Flash statistics
|
||||
flash_assists: z.number().int().nonnegative().optional(),
|
||||
flash_duration_enemy: z.number().nonnegative().optional(),
|
||||
flash_duration_team: z.number().nonnegative().optional(),
|
||||
flash_duration_self: z.number().nonnegative().optional(),
|
||||
flash_total_enemy: z.number().int().nonnegative().optional(),
|
||||
flash_total_team: z.number().int().nonnegative().optional(),
|
||||
flash_total_self: z.number().int().nonnegative().optional(),
|
||||
|
||||
// Other
|
||||
crosshair: z.string().optional(),
|
||||
color: z.enum(['green', 'yellow', 'purple', 'blue', 'orange', 'grey']).optional(),
|
||||
avg_ping: z.number().nonnegative().optional()
|
||||
});
|
||||
|
||||
/** Match schema */
|
||||
export const matchSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
share_code: z
|
||||
.string()
|
||||
.regex(/^CSGO-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/),
|
||||
map: z.string().min(1),
|
||||
date: z.string().datetime(),
|
||||
score_team_a: z.number().int().nonnegative(),
|
||||
score_team_b: z.number().int().nonnegative(),
|
||||
duration: z.number().int().positive(),
|
||||
match_result: z.number().int().min(0).max(2), // 0 = tie, 1 = team_a win, 2 = team_b win
|
||||
max_rounds: z.number().int().positive(),
|
||||
demo_parsed: z.boolean(),
|
||||
vac_present: z.boolean(),
|
||||
gameban_present: z.boolean(),
|
||||
tick_rate: z.number().positive(),
|
||||
players: z.array(matchPlayerSchema).optional()
|
||||
});
|
||||
|
||||
/** MatchListItem schema */
|
||||
export const matchListItemSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
map: z.string().min(1),
|
||||
date: z.string().datetime(),
|
||||
score_team_a: z.number().int().nonnegative(),
|
||||
score_team_b: z.number().int().nonnegative(),
|
||||
duration: z.number().int().positive(),
|
||||
demo_parsed: z.boolean(),
|
||||
player_count: z.number().int().min(2).max(10)
|
||||
});
|
||||
|
||||
/** Parser functions for safe data validation */
|
||||
export const parseMatch = (data: unknown) => matchSchema.parse(data);
|
||||
export const parseMatchSafe = (data: unknown) => matchSchema.safeParse(data);
|
||||
export const parseMatchPlayer = (data: unknown) => matchPlayerSchema.parse(data);
|
||||
export const parseMatchListItem = (data: unknown) => matchListItemSchema.parse(data);
|
||||
|
||||
/** Infer TypeScript types from schemas */
|
||||
export type MatchSchema = z.infer<typeof matchSchema>;
|
||||
export type MatchPlayerSchema = z.infer<typeof matchPlayerSchema>;
|
||||
export type MatchListItemSchema = z.infer<typeof matchListItemSchema>;
|
||||
69
src/lib/schemas/message.schema.ts
Normal file
69
src/lib/schemas/message.schema.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schemas for Message/Chat data models
|
||||
*/
|
||||
|
||||
/** Message schema */
|
||||
export const messageSchema = z.object({
|
||||
message: z.string(),
|
||||
all_chat: z.boolean(),
|
||||
tick: z.number().int().nonnegative(),
|
||||
match_player_id: z.number().positive().optional(),
|
||||
player_id: z.number().positive().optional(),
|
||||
player_name: z.string().optional(),
|
||||
round: z.number().int().positive().optional(),
|
||||
timestamp: z.string().datetime().optional()
|
||||
});
|
||||
|
||||
/** MatchChatResponse schema */
|
||||
export const matchChatResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
messages: z.array(messageSchema)
|
||||
});
|
||||
|
||||
/** EnrichedMessage schema (with player data) */
|
||||
export const enrichedMessageSchema = messageSchema.extend({
|
||||
player_name: z.string().min(1),
|
||||
player_avatar: z.string().url().optional(),
|
||||
team_id: z.number().int().min(2).max(3).optional(),
|
||||
round: z.number().int().positive()
|
||||
});
|
||||
|
||||
/** ChatFilter schema */
|
||||
export const chatFilterSchema = z.object({
|
||||
player_id: z.number().positive().optional(),
|
||||
chat_type: z.enum(['all', 'team', 'all_chat']).optional(),
|
||||
round: z.number().int().positive().optional(),
|
||||
search: z.string().optional()
|
||||
});
|
||||
|
||||
/** ChatStats schema */
|
||||
export const chatStatsSchema = z.object({
|
||||
total_messages: z.number().int().nonnegative(),
|
||||
team_chat_count: z.number().int().nonnegative(),
|
||||
all_chat_count: z.number().int().nonnegative(),
|
||||
messages_per_player: z.record(z.number().int().nonnegative()),
|
||||
most_active_player: z.object({
|
||||
player_id: z.number().positive(),
|
||||
message_count: z.number().int().positive()
|
||||
})
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
export const parseMessage = (data: unknown) => messageSchema.parse(data);
|
||||
export const parseMatchChat = (data: unknown) => matchChatResponseSchema.parse(data);
|
||||
export const parseEnrichedMessage = (data: unknown) => enrichedMessageSchema.parse(data);
|
||||
export const parseChatFilter = (data: unknown) => chatFilterSchema.parse(data);
|
||||
export const parseChatStats = (data: unknown) => chatStatsSchema.parse(data);
|
||||
|
||||
/** Safe parser functions */
|
||||
export const parseMessageSafe = (data: unknown) => messageSchema.safeParse(data);
|
||||
export const parseMatchChatSafe = (data: unknown) => matchChatResponseSchema.safeParse(data);
|
||||
|
||||
/** Infer TypeScript types */
|
||||
export type MessageSchema = z.infer<typeof messageSchema>;
|
||||
export type MatchChatResponseSchema = z.infer<typeof matchChatResponseSchema>;
|
||||
export type EnrichedMessageSchema = z.infer<typeof enrichedMessageSchema>;
|
||||
export type ChatFilterSchema = z.infer<typeof chatFilterSchema>;
|
||||
export type ChatStatsSchema = z.infer<typeof chatStatsSchema>;
|
||||
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>;
|
||||
62
src/lib/schemas/roundStats.schema.ts
Normal file
62
src/lib/schemas/roundStats.schema.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schemas for Round Statistics data models
|
||||
*/
|
||||
|
||||
/** RoundStats schema */
|
||||
export const roundStatsSchema = z.object({
|
||||
round: z.number().int().positive(),
|
||||
bank: z.number().int().nonnegative(),
|
||||
equipment: z.number().int().nonnegative(),
|
||||
spent: z.number().int().nonnegative(),
|
||||
kills_in_round: z.number().int().nonnegative().optional(),
|
||||
damage_in_round: z.number().int().nonnegative().optional(),
|
||||
match_player_id: z.number().positive().optional(),
|
||||
player_id: z.number().positive().optional()
|
||||
});
|
||||
|
||||
/** RoundDetail schema (with player breakdown) */
|
||||
export const roundDetailSchema = z.object({
|
||||
round: z.number().int().positive(),
|
||||
winner: z.number().int().min(2).max(3), // 2 = T, 3 = CT
|
||||
win_reason: z.string(),
|
||||
players: z.array(roundStatsSchema)
|
||||
});
|
||||
|
||||
/** MatchRoundsResponse schema */
|
||||
export const matchRoundsResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
rounds: z.array(roundDetailSchema)
|
||||
});
|
||||
|
||||
/** TeamRoundStats schema */
|
||||
export const teamRoundStatsSchema = z.object({
|
||||
round: z.number().int().positive(),
|
||||
team_id: z.number().int().min(2).max(3),
|
||||
total_bank: z.number().int().nonnegative(),
|
||||
total_equipment: z.number().int().nonnegative(),
|
||||
avg_equipment: z.number().nonnegative(),
|
||||
total_spent: z.number().int().nonnegative(),
|
||||
winner: z.number().int().min(2).max(3).optional(),
|
||||
win_reason: z
|
||||
.enum(['elimination', 'bomb_defused', 'bomb_exploded', 'time', 'target_saved'])
|
||||
.optional(),
|
||||
buy_type: z.enum(['eco', 'semi-eco', 'force', 'full']).optional()
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
export const parseRoundStats = (data: unknown) => roundStatsSchema.parse(data);
|
||||
export const parseRoundDetail = (data: unknown) => roundDetailSchema.parse(data);
|
||||
export const parseMatchRounds = (data: unknown) => matchRoundsResponseSchema.parse(data);
|
||||
export const parseTeamRoundStats = (data: unknown) => teamRoundStatsSchema.parse(data);
|
||||
|
||||
/** Safe parser functions */
|
||||
export const parseRoundStatsSafe = (data: unknown) => roundStatsSchema.safeParse(data);
|
||||
export const parseMatchRoundsSafe = (data: unknown) => matchRoundsResponseSchema.safeParse(data);
|
||||
|
||||
/** Infer TypeScript types */
|
||||
export type RoundStatsSchema = z.infer<typeof roundStatsSchema>;
|
||||
export type RoundDetailSchema = z.infer<typeof roundDetailSchema>;
|
||||
export type MatchRoundsResponseSchema = z.infer<typeof matchRoundsResponseSchema>;
|
||||
export type TeamRoundStatsSchema = z.infer<typeof teamRoundStatsSchema>;
|
||||
66
src/lib/schemas/weapon.schema.ts
Normal file
66
src/lib/schemas/weapon.schema.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Zod schemas for Weapon data models
|
||||
*/
|
||||
|
||||
/** Weapon schema */
|
||||
export const weaponSchema = z.object({
|
||||
victim: z.number().positive(),
|
||||
dmg: z.number().int().nonnegative(),
|
||||
eq_type: z.number().int().positive(),
|
||||
hit_group: z.number().int().min(0).max(7), // 0-7 hit groups
|
||||
match_player_id: z.number().positive().optional()
|
||||
});
|
||||
|
||||
/** Hit groups breakdown schema */
|
||||
export const hitGroupsSchema = z.object({
|
||||
head: z.number().int().nonnegative(),
|
||||
chest: z.number().int().nonnegative(),
|
||||
stomach: z.number().int().nonnegative(),
|
||||
left_arm: z.number().int().nonnegative(),
|
||||
right_arm: z.number().int().nonnegative(),
|
||||
left_leg: z.number().int().nonnegative(),
|
||||
right_leg: z.number().int().nonnegative()
|
||||
});
|
||||
|
||||
/** WeaponStats schema */
|
||||
export const weaponStatsSchema = z.object({
|
||||
eq_type: z.number().int().positive(),
|
||||
weapon_name: z.string().min(1),
|
||||
kills: z.number().int().nonnegative(),
|
||||
damage: z.number().int().nonnegative(),
|
||||
hits: z.number().int().nonnegative(),
|
||||
hit_groups: hitGroupsSchema,
|
||||
headshot_pct: z.number().nonnegative().optional(),
|
||||
accuracy: z.number().nonnegative().optional()
|
||||
});
|
||||
|
||||
/** PlayerWeaponStats schema */
|
||||
export const playerWeaponStatsSchema = z.object({
|
||||
player_id: z.number().positive(),
|
||||
weapon_stats: z.array(weaponStatsSchema)
|
||||
});
|
||||
|
||||
/** MatchWeaponsResponse schema */
|
||||
export const matchWeaponsResponseSchema = z.object({
|
||||
match_id: z.number().positive(),
|
||||
weapons: z.array(playerWeaponStatsSchema)
|
||||
});
|
||||
|
||||
/** Parser functions */
|
||||
export const parseWeapon = (data: unknown) => weaponSchema.parse(data);
|
||||
export const parseWeaponStats = (data: unknown) => weaponStatsSchema.parse(data);
|
||||
export const parsePlayerWeaponStats = (data: unknown) => playerWeaponStatsSchema.parse(data);
|
||||
export const parseMatchWeapons = (data: unknown) => matchWeaponsResponseSchema.parse(data);
|
||||
|
||||
/** Safe parser functions */
|
||||
export const parseWeaponSafe = (data: unknown) => weaponSchema.safeParse(data);
|
||||
export const parseMatchWeaponsSafe = (data: unknown) => matchWeaponsResponseSchema.safeParse(data);
|
||||
|
||||
/** Infer TypeScript types */
|
||||
export type WeaponSchema = z.infer<typeof weaponSchema>;
|
||||
export type HitGroupsSchema = z.infer<typeof hitGroupsSchema>;
|
||||
export type WeaponStatsSchema = z.infer<typeof weaponStatsSchema>;
|
||||
export type PlayerWeaponStatsSchema = z.infer<typeof playerWeaponStatsSchema>;
|
||||
export type MatchWeaponsResponseSchema = z.infer<typeof matchWeaponsResponseSchema>;
|
||||
Reference in New Issue
Block a user