feat(backend): 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 <noreply@anthropic.com>
This commit is contained in:
Masahiko AMANO 2026-04-03 18:28:33 +03:00
parent dbdc80b3a0
commit 1d341eef24
12 changed files with 290 additions and 1 deletions

5
backend/go.mod Normal file
View File

@ -0,0 +1,5 @@
module tanabata/backend
go 1.21
require github.com/google/uuid v1.6.0

2
backend/go.sum Normal file
View File

@ -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=

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -164,7 +164,7 @@ CREATE TABLE data.files (
content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken) content_datetime timestamptz NOT NULL DEFAULT clock_timestamp(), -- content datetime (e.g. photo taken)
notes text, notes text,
metadata jsonb, -- user-editable key-value data 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) phash bigint, -- perceptual hash for duplicate detection (future)
creator_id smallint NOT NULL REFERENCES core.users(id) creator_id smallint NOT NULL REFERENCES core.users(id)
ON UPDATE CASCADE ON DELETE RESTRICT, ON UPDATE CASCADE ON DELETE RESTRICT,