From f043d38eb29ab410e57d2bde17c49d88fbfc7d42 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Fri, 3 Apr 2026 18:28:33 +0300 Subject: [PATCH] feat: initialize Go module and implement domain layer - Add go.mod (module tanabata/backend, Go 1.21) with uuid dependency - Implement internal/domain: File, Tag, TagRule, Category, Pool, PoolFile, User, Session, Permission, ObjectType, AuditEntry + all pagination types - Add domain error sentinels (ErrNotFound, ErrForbidden, etc.) - Add context helpers WithUser/UserFromContext for JWT propagation - Fix migration: remove redundant DEFAULT on exif jsonb column Co-Authored-By: Claude Sonnet 4.6 --- backend/go.mod | 5 +++ backend/go.sum | 2 ++ backend/internal/domain/acl.go | 19 ++++++++++ backend/internal/domain/audit.go | 46 ++++++++++++++++++++++++ backend/internal/domain/category.go | 21 +++++++++++ backend/internal/domain/context.go | 24 +++++++++++++ backend/internal/domain/errors.go | 13 +++++++ backend/internal/domain/file.go | 54 +++++++++++++++++++++++++++++ backend/internal/domain/pool.go | 33 ++++++++++++++++++ backend/internal/domain/tag.go | 33 ++++++++++++++++++ backend/internal/domain/user.go | 39 +++++++++++++++++++++ backend/migrations/001_init.sql | 2 +- 12 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/domain/acl.go create mode 100644 backend/internal/domain/audit.go create mode 100644 backend/internal/domain/category.go create mode 100644 backend/internal/domain/context.go create mode 100644 backend/internal/domain/errors.go create mode 100644 backend/internal/domain/file.go create mode 100644 backend/internal/domain/pool.go create mode 100644 backend/internal/domain/tag.go create mode 100644 backend/internal/domain/user.go diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..21bf38a --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,5 @@ +module tanabata/backend + +go 1.21 + +require github.com/google/uuid v1.6.0 diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..7790d7c --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/backend/internal/domain/acl.go b/backend/internal/domain/acl.go new file mode 100644 index 0000000..0a660d8 --- /dev/null +++ b/backend/internal/domain/acl.go @@ -0,0 +1,19 @@ +package domain + +import "github.com/google/uuid" + +// ObjectType is a reference entity (file, tag, category, pool). +type ObjectType struct { + ID int16 + Name string +} + +// Permission represents a per-object access entry for a user. +type Permission struct { + UserID int16 + UserName string // denormalized + ObjectTypeID int16 + ObjectID uuid.UUID + CanView bool + CanEdit bool +} diff --git a/backend/internal/domain/audit.go b/backend/internal/domain/audit.go new file mode 100644 index 0000000..faeb469 --- /dev/null +++ b/backend/internal/domain/audit.go @@ -0,0 +1,46 @@ +package domain + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// ActionType is a reference entity for auditable user actions. +type ActionType struct { + ID int16 + Name string +} + +// AuditEntry is a single audit log record. +type AuditEntry struct { + ID int64 + UserID int16 + UserName string // denormalized + Action string // action type name, e.g. "file_create" + ObjectType *string + ObjectID *uuid.UUID + Details json.RawMessage + PerformedAt time.Time +} + +// AuditPage is an offset-based page of audit log entries. +type AuditPage struct { + Items []AuditEntry + Total int + Offset int + Limit int +} + +// AuditFilter holds filter parameters for querying the audit log. +type AuditFilter struct { + UserID *int16 + Action string + ObjectType string + ObjectID *uuid.UUID + From *time.Time + To *time.Time + Offset int + Limit int +} diff --git a/backend/internal/domain/category.go b/backend/internal/domain/category.go new file mode 100644 index 0000000..e614f39 --- /dev/null +++ b/backend/internal/domain/category.go @@ -0,0 +1,21 @@ +package domain + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// Category is a logical grouping of tags. +type Category struct { + ID uuid.UUID + Name string + Notes *string + Color *string // 6-char hex + Metadata json.RawMessage + CreatorID int16 + CreatorName string // denormalized + IsPublic bool + CreatedAt time.Time // extracted from UUID v7 +} diff --git a/backend/internal/domain/context.go b/backend/internal/domain/context.go new file mode 100644 index 0000000..2998c9a --- /dev/null +++ b/backend/internal/domain/context.go @@ -0,0 +1,24 @@ +package domain + +import "context" + +type ctxKey int + +const userKey ctxKey = iota + +type contextUser struct { + ID int16 + IsAdmin bool +} + +func WithUser(ctx context.Context, userID int16, isAdmin bool) context.Context { + return context.WithValue(ctx, userKey, contextUser{ID: userID, IsAdmin: isAdmin}) +} + +func UserFromContext(ctx context.Context) (userID int16, isAdmin bool) { + u, ok := ctx.Value(userKey).(contextUser) + if !ok { + return 0, false + } + return u.ID, u.IsAdmin +} diff --git a/backend/internal/domain/errors.go b/backend/internal/domain/errors.go new file mode 100644 index 0000000..93d4cae --- /dev/null +++ b/backend/internal/domain/errors.go @@ -0,0 +1,13 @@ +package domain + +import "errors" + +// Sentinel domain errors. Handlers map these to HTTP status codes. +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") +) diff --git a/backend/internal/domain/file.go b/backend/internal/domain/file.go new file mode 100644 index 0000000..7fb6f12 --- /dev/null +++ b/backend/internal/domain/file.go @@ -0,0 +1,54 @@ +package domain + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// MIMEType holds MIME whitelist data. +type MIMEType struct { + ID int16 + Name string + Extension string +} + +// File represents a managed file record. +type File struct { + ID uuid.UUID + OriginalName *string + MIMEType string // denormalized from core.mime_types + MIMEExtension string // denormalized from core.mime_types + ContentDatetime time.Time + Notes *string + Metadata json.RawMessage + EXIF json.RawMessage + PHash *int64 + CreatorID int16 + CreatorName string // denormalized from core.users + IsPublic bool + IsDeleted bool + CreatedAt time.Time // extracted from UUID v7 + Tags []Tag // loaded with the file +} + +// FileListParams holds all parameters for listing/filtering files. +type FileListParams struct { + Filter string + Sort string + Order string + Cursor string + Anchor *uuid.UUID + Direction string // "forward" or "backward" + Limit int + Trash bool + Search string +} + +// FilePage is the result of a cursor-based file listing. +type FilePage struct { + Items []File + NextCursor *string + PrevCursor *string +} diff --git a/backend/internal/domain/pool.go b/backend/internal/domain/pool.go new file mode 100644 index 0000000..e27c8ec --- /dev/null +++ b/backend/internal/domain/pool.go @@ -0,0 +1,33 @@ +package domain + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// Pool is an ordered collection of files. +type Pool struct { + ID uuid.UUID + Name string + Notes *string + Metadata json.RawMessage + CreatorID int16 + CreatorName string // denormalized + IsPublic bool + FileCount int + CreatedAt time.Time // extracted from UUID v7 +} + +// PoolFile is a File with its ordering position within a pool. +type PoolFile struct { + File + Position int +} + +// PoolFilePage is the result of a cursor-based pool file listing. +type PoolFilePage struct { + Items []PoolFile + NextCursor *string +} diff --git a/backend/internal/domain/tag.go b/backend/internal/domain/tag.go new file mode 100644 index 0000000..912cc95 --- /dev/null +++ b/backend/internal/domain/tag.go @@ -0,0 +1,33 @@ +package domain + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" +) + +// Tag represents a file label. +type Tag struct { + ID uuid.UUID + Name string + Notes *string + Color *string // 6-char hex, e.g. "5DCAA5" + CategoryID *uuid.UUID + CategoryName *string // denormalized + CategoryColor *string // denormalized + Metadata json.RawMessage + CreatorID int16 + CreatorName string // denormalized + IsPublic bool + CreatedAt time.Time // extracted from UUID v7 +} + +// TagRule defines an auto-tagging rule: when WhenTagID is applied, +// ThenTagID is automatically applied as well. +type TagRule struct { + WhenTagID uuid.UUID + ThenTagID uuid.UUID + ThenTagName string // denormalized + IsActive bool +} diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go new file mode 100644 index 0000000..8924873 --- /dev/null +++ b/backend/internal/domain/user.go @@ -0,0 +1,39 @@ +package domain + +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 +} + +// Session is an active user session. +type Session struct { + ID int + TokenHash string + UserID int16 + UserAgent string + StartedAt time.Time + ExpiresAt *time.Time + LastActivity time.Time + IsCurrent bool // true when this session matches the caller's token +} + +// OffsetPage is a generic offset-based page of users. +type UserPage struct { + Items []User + Total int + Offset int + Limit int +} + +// SessionList is a list of sessions with a total count. +type SessionList struct { + Items []Session + Total int +} diff --git a/backend/migrations/001_init.sql b/backend/migrations/001_init.sql index 409e55e..92774d0 100644 --- a/backend/migrations/001_init.sql +++ b/backend/migrations/001_init.sql @@ -164,7 +164,7 @@ CREATE TABLE data.files ( content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken) notes text, metadata jsonb, -- user-editable key-value data - exif jsonb NOT NULL DEFAULT '{}'::jsonb, -- EXIF data extracted at upload (immutable) + exif jsonb NOT NULL, -- EXIF data extracted at upload (immutable) phash bigint, -- perceptual hash for duplicate detection (future) creator_id smallint NOT NULL REFERENCES core.users(id) ON UPDATE CASCADE ON DELETE RESTRICT,