feat(typescript): add core infrastructure modules
- Add centralized API type definitions (types/api.d.ts) - Interfaces for all API responses matching Go handler JSON - Snake_case field names matching actual API responses - Source file references in comments for verification - Add API client module (api.ts) - Procedural get/post/put/delete functions - Automatic auth header injection - Exported to window for cross-module access - Add DOM utilities (dom.ts) - escapeHtml for safe HTML rendering - querySelector wrappers with null checks - Element creation helpers - Add event delegation helpers (events.ts) - Reusable event delegation pattern - Data attribute selectors for dynamic content - Add localStorage wrapper (storage.ts) - Type-safe token management - Theme persistence helpers
This commit is contained in:
Vendored
+313
@@ -0,0 +1,313 @@
|
||||
// ============================================
|
||||
// API Type Definitions
|
||||
// ============================================
|
||||
// These types match the JSON responses from /api/* endpoints.
|
||||
// Source of truth: Check what the endpoint ACTUALLY returns:
|
||||
// 1. Database layer: internal/database/queries.sql.go (SearchMediaItemsRow, etc.)
|
||||
// 2. Handler structs: internal/handlers/*.go (check json:"..." tags)
|
||||
// 3. Test by calling endpoint and inspecting JSON response
|
||||
//
|
||||
// When API contracts change:
|
||||
// 1. Find the endpoint function in internal/handlers/*.go
|
||||
// 2. Check what it returns (database row or struct)
|
||||
// 3. Check the JSON tags: `json:"field_name"`
|
||||
// 4. Map pgtype fields to TypeScript types:
|
||||
// - pgtype.Text → string | undefined
|
||||
// - pgtype.UUID → string
|
||||
// - pgtype.Timestamp → string (ISO datetime)
|
||||
// - pgtype.Numeric → number or string (for precision)
|
||||
// 5. Update the interface below with snake_case field names
|
||||
// 6. Run Bruno tests to verify
|
||||
// ============================================
|
||||
|
||||
// Matches database.SearchMediaItemsRow from /api/media-items/search
|
||||
// Source: internal/database/queries.sql.go:6123-6170 SearchMediaItemsRow
|
||||
// Endpoint: internal/handlers/media.go:SearchMediaItems()
|
||||
// Note: internal/handlers/search.go has an unused MediaItemSummary - ignore it
|
||||
// Used in: search.ts
|
||||
export interface MediaItemSummary {
|
||||
id: string;
|
||||
library_id: string;
|
||||
title: string;
|
||||
author?: string;
|
||||
isbn?: string;
|
||||
description?: string;
|
||||
file_path: string;
|
||||
file_size?: number;
|
||||
mime_type?: string;
|
||||
cover_image_path?: string;
|
||||
series?: string;
|
||||
series_number?: number;
|
||||
tags?: string[];
|
||||
asin?: string;
|
||||
date_published?: string;
|
||||
publisher?: string;
|
||||
contributors?: string[];
|
||||
language?: string;
|
||||
edition?: string;
|
||||
page_count?: number;
|
||||
genre?: string;
|
||||
copyright_year?: number;
|
||||
goodreads_id?: string;
|
||||
openlibrary_id?: string;
|
||||
google_books_id?: string;
|
||||
added_by_admin_id?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
format_group: string;
|
||||
format_mimetype?: string;
|
||||
is_reflowable?: boolean;
|
||||
has_fixed_layout?: boolean;
|
||||
total_characters?: number;
|
||||
chapter_count?: number;
|
||||
entitlement_id?: string;
|
||||
revision_number?: number;
|
||||
kobo_content_id?: string;
|
||||
kobo_metadata?: string;
|
||||
tags_search?: string[];
|
||||
contributors_search?: string[];
|
||||
file_sha256?: string;
|
||||
opf_identifier?: string;
|
||||
opf_uuid?: string;
|
||||
hash_confidence?: string;
|
||||
library_name: string;
|
||||
library_type_name: string;
|
||||
}
|
||||
|
||||
// Matches handlers.CollectionData / CollectionResponse JSON response
|
||||
// Source: internal/handlers/collections.go:123-131 CollectionResponse
|
||||
// Used in: collections.ts
|
||||
export interface CollectionData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
auto_assign_rules?: unknown;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Matches handlers.BookInfo JSON response (internal/handlers/collections.go:66-71)
|
||||
// JSON tags: media_item_id, title, author, cover_image_path
|
||||
// Used in: collections.templ (server-rendered), collections.ts
|
||||
export interface BookInfo {
|
||||
media_item_id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
cover_image_path: string;
|
||||
}
|
||||
|
||||
// Matches handlers.UnlinkedBookData JSON response
|
||||
// Used in: unlinked_books.ts, unlinked_books.templ
|
||||
export interface UnlinkedBookData {
|
||||
progress_id: string;
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
device_type: 'koreader' | 'kobo' | 'web';
|
||||
title_from_device: string;
|
||||
file_path: string;
|
||||
sha256: string;
|
||||
last_sync_time: string;
|
||||
confidence_score: number;
|
||||
potential_matches: PotentialMatchData[];
|
||||
}
|
||||
|
||||
export interface PotentialMatchData {
|
||||
media_item_id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
confidence: number;
|
||||
cover_image_path?: string;
|
||||
}
|
||||
|
||||
// Matches collection rule objects
|
||||
// Used in: collection_rules.ts
|
||||
export interface CollectionRule {
|
||||
id: string;
|
||||
field: 'genre' | 'series' | 'author' | 'language' | 'publisher' | 'copyright_year' | 'tags';
|
||||
operator: 'equals' | 'not_equals' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'greater_than' | 'less_than';
|
||||
value: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
// Matches API test rule responses
|
||||
// Used in: collection_rules.ts (test results)
|
||||
export interface TestRuleMatch {
|
||||
title: string;
|
||||
author: string;
|
||||
cover_image_path?: string;
|
||||
}
|
||||
|
||||
// Matches handlers.SearchResponse (internal/handlers/search.go)
|
||||
export interface SearchResponse {
|
||||
results: SearchBookResponse[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SearchBookResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
authors: SearchAuthor[];
|
||||
}
|
||||
|
||||
export interface SearchAuthor {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}
|
||||
|
||||
// Matches AuthResponse (internal/handlers/auth.go:59-65)
|
||||
export interface AuthResponse {
|
||||
access_token: string;
|
||||
refresh_token?: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
user: UserProfile;
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
role: string;
|
||||
theme?: string;
|
||||
}
|
||||
|
||||
// Matches handlers.ReadingStatsResponse (internal/handlers/analytics.go:26-35)
|
||||
// Used in: analytics.ts
|
||||
export interface ReadingStatsResponse {
|
||||
total_books_read: number;
|
||||
total_pages_read: number;
|
||||
total_reading_time_minutes: number;
|
||||
average_session_time_minutes: number;
|
||||
longest_session_minutes: number;
|
||||
most_active_day_of_week: string;
|
||||
completion_rate: number;
|
||||
daily_reading_minutes: DailyReading[];
|
||||
}
|
||||
|
||||
export interface DailyReading {
|
||||
date: string;
|
||||
minutes: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
// Matches handlers.DeviceUsageResponse (internal/handlers/analytics.go:43-45)
|
||||
// Note: Response is wrapped: { devices: DeviceUsage[] }
|
||||
// Used in: analytics.ts
|
||||
export interface DeviceUsageResponse {
|
||||
devices: DeviceUsage[];
|
||||
}
|
||||
|
||||
export interface DeviceUsage {
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
device_type: string;
|
||||
sync_count: number;
|
||||
last_sync: string;
|
||||
total_time_seconds: number;
|
||||
total_time_minutes: number;
|
||||
}
|
||||
|
||||
// Matches handlers.PopularBooksResponse (internal/handlers/analytics.go:57-59)
|
||||
// Note: Response is wrapped: { books: PopularBook[] }
|
||||
// Used in: analytics.ts
|
||||
export interface PopularBooksResponse {
|
||||
books: PopularBook[];
|
||||
}
|
||||
|
||||
export interface PopularBook {
|
||||
media_item_id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
read_count: number;
|
||||
avg_completion: number;
|
||||
last_read: string;
|
||||
}
|
||||
|
||||
// Matches handlers.QueueItemResponse (internal/handlers/queue.go:35-51)
|
||||
// Used in: queue.ts
|
||||
export interface QueueItemResponse {
|
||||
id: string;
|
||||
device_id: string;
|
||||
device_name: string;
|
||||
device_type: string;
|
||||
media_item_id?: string;
|
||||
media_title?: string;
|
||||
user_email: string;
|
||||
sync_type: string;
|
||||
priority: number;
|
||||
attempts: number;
|
||||
max_attempts: number;
|
||||
status: string;
|
||||
error_message?: string;
|
||||
created_at: string;
|
||||
processed_at?: string;
|
||||
}
|
||||
|
||||
// Matches handlers.QueueStatsResponse (internal/handlers/queue.go:27-33)
|
||||
// Used in: queue.ts
|
||||
export interface QueueStatsResponse {
|
||||
pending_count: number;
|
||||
processing_count: number;
|
||||
failed_count: number;
|
||||
completed_count: number;
|
||||
total_count: number;
|
||||
}
|
||||
|
||||
// Matches handlers.ConflictDetailResponse (internal/handlers/conflicts.go:42-53)
|
||||
// Used in: conflicts.ts
|
||||
export interface ConflictDetailResponse {
|
||||
id: string;
|
||||
media_item_id: string;
|
||||
media_item_title: string;
|
||||
conflict_type: string;
|
||||
conflict_data: Record<string, ConflictSourceData>;
|
||||
resolution_status: string;
|
||||
resolution_data?: Record<string, unknown>;
|
||||
resolved_by?: string;
|
||||
resolved_at?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Matches handlers.ConflictSourceData (internal/handlers/conflicts.go:36-40)
|
||||
export interface ConflictSourceData {
|
||||
source: string;
|
||||
timestamp: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Matches handlers.ConflictListResponse (internal/handlers/conflicts.go:55-59)
|
||||
// Used in: conflicts.ts
|
||||
export interface ConflictListResponse {
|
||||
conflicts: ConflictDetailResponse[];
|
||||
total: number;
|
||||
unresolved: number;
|
||||
}
|
||||
|
||||
// Matches handlers.ConflictResolveResponse (internal/handlers/conflicts.go:61-65)
|
||||
// Used in: conflicts.ts
|
||||
export interface ConflictResolveResponse {
|
||||
conflict_resolved: boolean;
|
||||
applied_to: Record<string, boolean>;
|
||||
devices_synced: string[];
|
||||
}
|
||||
|
||||
// Matches handlers.BulkResolveResponse (internal/handlers/conflicts.go:424-429)
|
||||
// Used in: conflicts.ts
|
||||
export interface BulkResolveResponse {
|
||||
results: ConflictResult[];
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
// Matches handlers.ConflictResult (internal/handlers/conflicts.go:431-436)
|
||||
export interface ConflictResult {
|
||||
conflict_id: string;
|
||||
status: string;
|
||||
error?: string;
|
||||
winner?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user