refactor: strengthen domain layer types and add missing page types

- DomainError struct with Code() string method replaces plain errors.New
  sentinels; errors.Is() still works via pointer equality
- UUIDCreatedAt(uuid.UUID) time.Time helper extracts timestamp from UUID v7
- Add TagOffsetPage, CategoryOffsetPage, PoolOffsetPage
- FileListParams fields grouped with comments matching openapi.yaml params
- Fix mismatched comment on UserPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Masahiko AMANO 2026-04-04 00:06:44 +03:00
parent a9209ae3a3
commit 8dd2d631e5
6 changed files with 70 additions and 25 deletions

View File

@ -17,5 +17,13 @@ type Category struct {
CreatorID int16 CreatorID int16
CreatorName string // denormalized CreatorName string // denormalized
IsPublic bool IsPublic bool
CreatedAt time.Time // extracted from UUID v7 CreatedAt time.Time // extracted from UUID v7 via UUIDCreatedAt
}
// CategoryOffsetPage is an offset-based page of categories.
type CategoryOffsetPage struct {
Items []Category
Total int
Offset int
Limit int
} }

View File

@ -1,13 +1,21 @@
package domain package domain
import "errors" // DomainError is a typed domain error with a stable machine-readable code.
// Handlers map these codes to HTTP status codes.
type DomainError struct {
code string
message string
}
// Sentinel domain errors. Handlers map these to HTTP status codes. func (e *DomainError) Error() string { return e.message }
func (e *DomainError) Code() string { return e.code }
// Sentinel domain errors. Use errors.Is(err, domain.ErrNotFound) for matching.
var ( var (
ErrNotFound = errors.New("not found") ErrNotFound = &DomainError{"not_found", "not found"}
ErrForbidden = errors.New("forbidden") ErrForbidden = &DomainError{"forbidden", "forbidden"}
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = &DomainError{"unauthorized", "unauthorized"}
ErrConflict = errors.New("conflict") ErrConflict = &DomainError{"conflict", "conflict"}
ErrValidation = errors.New("validation error") ErrValidation = &DomainError{"validation_error", "validation error"}
ErrUnsupportedMIME = errors.New("unsupported MIME type") ErrUnsupportedMIME = &DomainError{"unsupported_mime", "unsupported MIME type"}
) )

View File

@ -29,21 +29,26 @@ type File struct {
CreatorName string // denormalized from core.users CreatorName string // denormalized from core.users
IsPublic bool IsPublic bool
IsDeleted bool IsDeleted bool
CreatedAt time.Time // extracted from UUID v7 CreatedAt time.Time // extracted from UUID v7 via UUIDCreatedAt
Tags []Tag // loaded with the file Tags []Tag // loaded with the file
} }
// FileListParams holds all parameters for listing/filtering files. // FileListParams holds all parameters for listing/filtering files.
type FileListParams struct { type FileListParams struct {
Filter string // Pagination
Sort string
Order string
Cursor string Cursor string
Anchor *uuid.UUID
Direction string // "forward" or "backward" Direction string // "forward" or "backward"
Anchor *uuid.UUID
Limit int Limit int
Trash bool
Search string // Sorting
Sort string // "content_datetime" | "created" | "original_name" | "mime"
Order string // "asc" | "desc"
// Filtering
Filter string // filter DSL expression
Search string // substring match on original_name
Trash bool // if true, return only soft-deleted files
} }
// FilePage is the result of a cursor-based file listing. // FilePage is the result of a cursor-based file listing.
@ -52,3 +57,11 @@ type FilePage struct {
NextCursor *string NextCursor *string
PrevCursor *string PrevCursor *string
} }
// UUIDCreatedAt extracts the creation timestamp embedded in a UUID v7.
// UUID v7 stores Unix milliseconds in the most-significant 48 bits.
func UUIDCreatedAt(id uuid.UUID) time.Time {
ms := int64(id[0])<<40 | int64(id[1])<<32 | int64(id[2])<<24 |
int64(id[3])<<16 | int64(id[4])<<8 | int64(id[5])
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)).UTC()
}

View File

@ -17,7 +17,7 @@ type Pool struct {
CreatorName string // denormalized CreatorName string // denormalized
IsPublic bool IsPublic bool
FileCount int FileCount int
CreatedAt time.Time // extracted from UUID v7 CreatedAt time.Time // extracted from UUID v7 via UUIDCreatedAt
} }
// PoolFile is a File with its ordering position within a pool. // PoolFile is a File with its ordering position within a pool.
@ -31,3 +31,11 @@ type PoolFilePage struct {
Items []PoolFile Items []PoolFile
NextCursor *string NextCursor *string
} }
// PoolOffsetPage is an offset-based page of pools.
type PoolOffsetPage struct {
Items []Pool
Total int
Offset int
Limit int
}

View File

@ -20,7 +20,7 @@ type Tag struct {
CreatorID int16 CreatorID int16
CreatorName string // denormalized CreatorName string // denormalized
IsPublic bool IsPublic bool
CreatedAt time.Time // extracted from UUID v7 CreatedAt time.Time // extracted from UUID v7 via UUIDCreatedAt
} }
// TagRule defines an auto-tagging rule: when WhenTagID is applied, // TagRule defines an auto-tagging rule: when WhenTagID is applied,
@ -31,3 +31,11 @@ type TagRule struct {
ThenTagName string // denormalized ThenTagName string // denormalized
IsActive bool IsActive bool
} }
// TagOffsetPage is an offset-based page of tags.
type TagOffsetPage struct {
Items []Tag
Total int
Offset int
Limit int
}

View File

@ -4,12 +4,12 @@ import "time"
// User is an application user. // User is an application user.
type User struct { type User struct {
ID int16 ID int16
Name string Name string
Password string // bcrypt hash; only populated when needed for auth Password string // bcrypt hash; only populated when needed for auth
IsAdmin bool IsAdmin bool
CanCreate bool CanCreate bool
IsBlocked bool IsBlocked bool
} }
// Session is an active user session. // Session is an active user session.
@ -24,7 +24,7 @@ type Session struct {
IsCurrent bool // true when this session matches the caller's token IsCurrent bool // true when this session matches the caller's token
} }
// OffsetPage is a generic offset-based page of users. // UserPage is an offset-based page of users.
type UserPage struct { type UserPage struct {
Items []User Items []User
Total int Total int