be consistent

This commit is contained in:
2026-07-03 20:35:45 -04:00
parent 1ff96ee9c8
commit 9158648f3f
15 changed files with 80 additions and 80 deletions
+2
View File
@@ -25,6 +25,8 @@ Regenerate:
- `cd frontend && pnpm run generate:api` - `cd frontend && pnpm run generate:api`
Notes: Notes:
- `format: date-time` diverges by generator: **oapi-codegen (Go)** emits `time.Time`, while **orval (TS)** keeps `string`. Adding it to an existing string field will break Go handlers until they supply a `time.Time` (see `parseTime` / `parseTimeAny` in `api/v1/utils.go`).
- Marking a schema field `required` flips the generated Go type from a pointer to a value; update every handler build site to drop the `&`. Only require a field when **all** endpoints sharing that schema populate it — `Progress` is populated with different subsets by its list vs single handlers.
- If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly. - If you add response headers in `api/v1/openapi.yaml` (for example `Set-Cookie`), `oapi-codegen` will generate typed response header structs in `api/v1/api.gen.go`; update the handler response values to populate those headers explicitly.
Examples of generated files: Examples of generated files:
+1 -9
View File
@@ -68,18 +68,10 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje
apiActivities := make([]Activity, len(activities)) apiActivities := make([]Activity, len(activities))
for i, a := range activities { for i, a := range activities {
// Convert StartTime from interface{} to string
startTimeStr := ""
if a.StartTime != nil {
if str, ok := a.StartTime.(string); ok {
startTimeStr = str
}
}
apiActivities[i] = Activity{ apiActivities[i] = Activity{
DocumentId: a.DocumentID, DocumentId: a.DocumentID,
DeviceId: a.DeviceID, DeviceId: a.DeviceID,
StartTime: startTimeStr, StartTime: parseTimeAny(a.StartTime),
Title: a.Title, Title: a.Title,
Author: a.Author, Author: a.Author,
Duration: a.Duration, Duration: a.Duration,
+29 -30
View File
@@ -145,15 +145,15 @@ func (e GetSearchParamsSource) Valid() bool {
// Activity defines model for Activity. // Activity defines model for Activity.
type Activity struct { type Activity struct {
Author *string `json:"author,omitempty"` Author *string `json:"author,omitempty"`
DeviceId string `json:"device_id"` DeviceId string `json:"device_id"`
DocumentId string `json:"document_id"` DocumentId string `json:"document_id"`
Duration int64 `json:"duration"` Duration int64 `json:"duration"`
EndPercentage float32 `json:"end_percentage"` EndPercentage float32 `json:"end_percentage"`
ReadPercentage float32 `json:"read_percentage"` ReadPercentage float32 `json:"read_percentage"`
StartPercentage float32 `json:"start_percentage"` StartPercentage float32 `json:"start_percentage"`
StartTime string `json:"start_time"` StartTime time.Time `json:"start_time"`
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
} }
// ActivityResponse defines model for ActivityResponse. // ActivityResponse defines model for ActivityResponse.
@@ -200,10 +200,10 @@ type DatabaseInfo struct {
// Device defines model for Device. // Device defines model for Device.
type Device struct { type Device struct {
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at"`
DeviceName *string `json:"device_name,omitempty"` DeviceName string `json:"device_name"`
Id *string `json:"id,omitempty"` Id string `json:"id"`
LastSynced *time.Time `json:"last_synced,omitempty"` LastSynced time.Time `json:"last_synced"`
} }
// DirectoryItem defines model for DirectoryItem. // DirectoryItem defines model for DirectoryItem.
@@ -356,15 +356,15 @@ type OperationType string
// Progress defines model for Progress. // Progress defines model for Progress.
type Progress struct { type Progress struct {
Author *string `json:"author,omitempty"` Author *string `json:"author,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt time.Time `json:"created_at"`
DeviceId *string `json:"device_id,omitempty"` DeviceId *string `json:"device_id,omitempty"`
DeviceName *string `json:"device_name,omitempty"` DeviceName string `json:"device_name"`
DocumentId *string `json:"document_id,omitempty"` DocumentId string `json:"document_id"`
Percentage *float64 `json:"percentage,omitempty"` Percentage float64 `json:"percentage"`
Progress *string `json:"progress,omitempty"` Progress *string `json:"progress,omitempty"`
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
UserId *string `json:"user_id,omitempty"` UserId string `json:"user_id"`
} }
// ProgressListResponse defines model for ProgressListResponse. // ProgressListResponse defines model for ProgressListResponse.
@@ -384,14 +384,13 @@ type ProgressResponse struct {
// SearchItem defines model for SearchItem. // SearchItem defines model for SearchItem.
type SearchItem struct { type SearchItem struct {
Author *string `json:"author,omitempty"` Author *string `json:"author,omitempty"`
FileSize *string `json:"file_size,omitempty"` FileSize *string `json:"file_size,omitempty"`
FileType *string `json:"file_type,omitempty"` FileType *string `json:"file_type,omitempty"`
Id *string `json:"id,omitempty"` Id *string `json:"id,omitempty"`
Language *string `json:"language,omitempty"` Language *string `json:"language,omitempty"`
Series *string `json:"series,omitempty"` Series *string `json:"series,omitempty"`
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
UploadDate *string `json:"upload_date,omitempty"`
} }
// SearchResponse defines model for SearchResponse. // SearchResponse defines model for SearchResponse.
+12 -2
View File
@@ -106,6 +106,12 @@ components:
created_at: created_at:
type: string type: string
format: date-time format: date-time
required:
- device_name
- percentage
- document_id
- user_id
- created_at
UpdateProgressRequest: UpdateProgressRequest:
type: object type: object
@@ -198,6 +204,7 @@ components:
type: string type: string
start_time: start_time:
type: string type: string
format: date-time
title: title:
type: string type: string
author: author:
@@ -240,8 +247,6 @@ components:
type: string type: string
file_size: file_size:
type: string type: string
upload_date:
type: string
SearchResponse: SearchResponse:
type: object type: object
@@ -384,6 +389,11 @@ components:
last_synced: last_synced:
type: string type: string
format: date-time format: date-time
required:
- id
- device_name
- created_at
- last_synced
SettingsResponse: SettingsResponse:
type: object type: object
+10 -10
View File
@@ -68,11 +68,11 @@ func (s *Server) GetProgressList(ctx context.Context, request GetProgressListReq
apiProgress[i] = Progress{ apiProgress[i] = Progress{
Title: row.Title, Title: row.Title,
Author: row.Author, Author: row.Author,
DeviceName: &row.DeviceName, DeviceName: row.DeviceName,
Percentage: &row.Percentage, Percentage: row.Percentage,
DocumentId: &row.DocumentID, DocumentId: row.DocumentID,
UserId: &row.UserID, UserId: row.UserID,
CreatedAt: parseTimePtr(row.CreatedAt), CreatedAt: parseTimeAny(row.CreatedAt),
} }
} }
@@ -105,13 +105,13 @@ func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObje
} }
apiProgress := Progress{ apiProgress := Progress{
DeviceName: &row.DeviceName, DeviceName: row.DeviceName,
DeviceId: &row.DeviceID, DeviceId: &row.DeviceID,
Percentage: &row.Percentage, Percentage: row.Percentage,
Progress: &row.Progress, Progress: &row.Progress,
DocumentId: &row.DocumentID, DocumentId: row.DocumentID,
UserId: &row.UserID, UserId: row.UserID,
CreatedAt: parseTimePtr(row.CreatedAt), CreatedAt: parseTime(row.CreatedAt),
} }
response := ProgressResponse{ response := ProgressResponse{
-1
View File
@@ -38,7 +38,6 @@ func (s *Server) GetSearch(ctx context.Context, request GetSearchRequestObject)
Series: ptrOf(item.Series), Series: ptrOf(item.Series),
FileType: ptrOf(item.FileType), FileType: ptrOf(item.FileType),
FileSize: ptrOf(item.FileSize), FileSize: ptrOf(item.FileSize),
UploadDate: ptrOf(item.UploadDate),
} }
} }
+8 -8
View File
@@ -29,10 +29,10 @@ func (s *Server) GetSettings(ctx context.Context, request GetSettingsRequestObje
apiDevices := make([]Device, len(devices)) apiDevices := make([]Device, len(devices))
for i, device := range devices { for i, device := range devices {
apiDevices[i] = Device{ apiDevices[i] = Device{
Id: &device.ID, Id: device.ID,
DeviceName: &device.DeviceName, DeviceName: device.DeviceName,
CreatedAt: parseTimePtr(device.CreatedAt), CreatedAt: parseTimeAny(device.CreatedAt),
LastSynced: parseTimePtr(device.LastSynced), LastSynced: parseTimeAny(device.LastSynced),
} }
} }
@@ -140,10 +140,10 @@ func (s *Server) UpdateSettings(ctx context.Context, request UpdateSettingsReque
apiDevices := make([]Device, len(devices)) apiDevices := make([]Device, len(devices))
for i, device := range devices { for i, device := range devices {
apiDevices[i] = Device{ apiDevices[i] = Device{
Id: &device.ID, Id: device.ID,
DeviceName: &device.DeviceName, DeviceName: device.DeviceName,
CreatedAt: parseTimePtr(device.CreatedAt), CreatedAt: parseTimeAny(device.CreatedAt),
LastSynced: parseTimePtr(device.LastSynced), LastSynced: parseTimeAny(device.LastSynced),
} }
} }
+8
View File
@@ -68,6 +68,14 @@ func parseTime(s string) time.Time {
return t return t
} }
// parseTimeAny parses an interface{} (from SQL) to time.Time, zero on failure.
func parseTimeAny(v interface{}) time.Time {
if s, ok := v.(string); ok {
return parseTime(s)
}
return time.Time{}
}
// parseTimePtr parses an interface{} (from SQL) to *time.Time // parseTimePtr parses an interface{} (from SQL) to *time.Time
func parseTimePtr(v interface{}) *time.Time { func parseTimePtr(v interface{}) *time.Time {
if v == nil { if v == nil {
+4 -4
View File
@@ -7,8 +7,8 @@
*/ */
export interface Device { export interface Device {
id?: string; id: string;
device_name?: string; device_name: string;
created_at?: string; created_at: string;
last_synced?: string; last_synced: string;
} }
+5 -5
View File
@@ -9,11 +9,11 @@
export interface Progress { export interface Progress {
title?: string; title?: string;
author?: string; author?: string;
device_name?: string; device_name: string;
device_id?: string; device_id?: string;
percentage?: number; percentage: number;
progress?: string; progress?: string;
document_id?: string; document_id: string;
user_id?: string; user_id: string;
created_at?: string; created_at: string;
} }
@@ -14,5 +14,4 @@ export interface SearchItem {
series?: string; series?: string;
file_type?: string; file_type?: string;
file_size?: string; file_size?: string;
upload_date?: string;
} }
+1 -1
View File
@@ -26,7 +26,7 @@ export default function ProgressPage() {
{ {
id: 'percentage', id: 'percentage',
header: 'Percentage', header: 'Percentage',
render: row => (typeof row.percentage === 'number' ? `${Math.round(row.percentage)}%` : '0%'), render: row => `${Math.round(row.percentage)}%`,
}, },
{ {
id: 'created_at', id: 'created_at',
-1
View File
@@ -77,7 +77,6 @@ describe('SearchPage', () => {
series: 'Earthsea', series: 'Earthsea',
file_type: 'epub', file_type: 'epub',
file_size: '1 MB', file_size: '1 MB',
upload_date: '2025-01-01',
}, },
], ],
}, },
-7
View File
@@ -6,7 +6,6 @@ import { Button, Table, type Column, TextInput, IconInput } from '../components'
import { inputClassName } from '../components/TextInput'; import { inputClassName } from '../components/TextInput';
import { useDebouncedState } from '../hooks/useDebouncedState'; import { useDebouncedState } from '../hooks/useDebouncedState';
import { Search2Icon, BookIcon } from '../icons'; import { Search2Icon, BookIcon } from '../icons';
import { formatDate } from '../utils/formatters';
import { dataForStatus } from '../utils/apiResponses'; import { dataForStatus } from '../utils/apiResponses';
const searchColumns: Column<SearchItem>[] = [ const searchColumns: Column<SearchItem>[] = [
@@ -18,12 +17,6 @@ const searchColumns: Column<SearchItem>[] = [
{ id: 'series', header: 'Series', render: item => item.series || 'N/A' }, { id: 'series', header: 'Series', render: item => item.series || 'N/A' },
{ id: 'type', header: 'Type', render: item => item.file_type || 'N/A' }, { id: 'type', header: 'Type', render: item => item.file_type || 'N/A' },
{ id: 'size', header: 'Size', render: item => item.file_size || 'N/A' }, { id: 'size', header: 'Size', render: item => item.file_size || 'N/A' },
{
id: 'date',
header: 'Date',
className: 'hidden md:table-cell',
render: item => formatDate(item.upload_date),
},
]; ];
interface SearchPageViewProps { interface SearchPageViewProps {
-1
View File
@@ -37,7 +37,6 @@ type SearchItem struct {
Series string Series string
FileType string FileType string
FileSize string FileSize string
UploadDate string
} }
type searchFunc func(query string) (searchResults []SearchItem, err error) type searchFunc func(query string) (searchResults []SearchItem, err error)