From 1e2a2a61deb014d7464c11cca2ad41e948c6e748 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Sat, 4 Apr 2026 00:06:44 +0300 Subject: [PATCH] refactor(backend): 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 --- backend/internal/domain/category.go | 10 +++++++++- backend/internal/domain/errors.go | 24 ++++++++++++++++-------- backend/internal/domain/file.go | 27 ++++++++++++++++++++------- backend/internal/domain/pool.go | 10 +++++++++- backend/internal/domain/tag.go | 10 +++++++++- backend/internal/domain/user.go | 14 +++++++------- 6 files changed, 70 insertions(+), 25 deletions(-) diff --git a/backend/internal/domain/category.go b/backend/internal/domain/category.go index e614f39..b1b7ecb 100644 --- a/backend/internal/domain/category.go +++ b/backend/internal/domain/category.go @@ -17,5 +17,13 @@ type Category struct { CreatorID int16 CreatorName string // denormalized 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 } diff --git a/backend/internal/domain/errors.go b/backend/internal/domain/errors.go index 93d4cae..f3f1d69 100644 --- a/backend/internal/domain/errors.go +++ b/backend/internal/domain/errors.go @@ -1,13 +1,21 @@ 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 ( - ErrNotFound = errors.New("not found") - ErrForbidden = errors.New("forbidden") - ErrUnauthorized = errors.New("unauthorized") - ErrConflict = errors.New("conflict") - ErrValidation = errors.New("validation error") - ErrUnsupportedMIME = errors.New("unsupported MIME type") + ErrNotFound = &DomainError{"not_found", "not found"} + ErrForbidden = &DomainError{"forbidden", "forbidden"} + ErrUnauthorized = &DomainError{"unauthorized", "unauthorized"} + ErrConflict = &DomainError{"conflict", "conflict"} + ErrValidation = &DomainError{"validation_error", "validation error"} + ErrUnsupportedMIME = &DomainError{"unsupported_mime", "unsupported MIME type"} ) diff --git a/backend/internal/domain/file.go b/backend/internal/domain/file.go index 7fb6f12..45f72f9 100644 --- a/backend/internal/domain/file.go +++ b/backend/internal/domain/file.go @@ -29,21 +29,26 @@ type File struct { CreatorName string // denormalized from core.users IsPublic 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 } // FileListParams holds all parameters for listing/filtering files. type FileListParams struct { - Filter string - Sort string - Order string + // Pagination Cursor string - Anchor *uuid.UUID Direction string // "forward" or "backward" + Anchor *uuid.UUID 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. @@ -52,3 +57,11 @@ type FilePage struct { NextCursor *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() +} diff --git a/backend/internal/domain/pool.go b/backend/internal/domain/pool.go index e27c8ec..bceb50f 100644 --- a/backend/internal/domain/pool.go +++ b/backend/internal/domain/pool.go @@ -17,7 +17,7 @@ type Pool struct { CreatorName string // denormalized IsPublic bool 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. @@ -31,3 +31,11 @@ type PoolFilePage struct { Items []PoolFile NextCursor *string } + +// PoolOffsetPage is an offset-based page of pools. +type PoolOffsetPage struct { + Items []Pool + Total int + Offset int + Limit int +} diff --git a/backend/internal/domain/tag.go b/backend/internal/domain/tag.go index 912cc95..4b06b44 100644 --- a/backend/internal/domain/tag.go +++ b/backend/internal/domain/tag.go @@ -20,7 +20,7 @@ type Tag struct { CreatorID int16 CreatorName string // denormalized 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, @@ -31,3 +31,11 @@ type TagRule struct { ThenTagName string // denormalized IsActive bool } + +// TagOffsetPage is an offset-based page of tags. +type TagOffsetPage struct { + Items []Tag + Total int + Offset int + Limit int +} diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go index 8924873..1809412 100644 --- a/backend/internal/domain/user.go +++ b/backend/internal/domain/user.go @@ -4,12 +4,12 @@ import "time" // User is an application user. type User struct { - ID int16 - Name string - Password string // bcrypt hash; only populated when needed for auth - IsAdmin bool - CanCreate bool - IsBlocked bool + ID int16 + Name string + Password string // bcrypt hash; only populated when needed for auth + IsAdmin bool + CanCreate bool + IsBlocked bool } // Session is an active user session. @@ -24,7 +24,7 @@ type Session struct { 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 { Items []User Total int