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:
parent
dbdc80b3a0
commit
1d341eef24
5
backend/go.mod
Normal file
5
backend/go.mod
Normal 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
2
backend/go.sum
Normal 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=
|
||||
19
backend/internal/domain/acl.go
Normal file
19
backend/internal/domain/acl.go
Normal 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
|
||||
}
|
||||
46
backend/internal/domain/audit.go
Normal file
46
backend/internal/domain/audit.go
Normal 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
|
||||
}
|
||||
21
backend/internal/domain/category.go
Normal file
21
backend/internal/domain/category.go
Normal 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
|
||||
}
|
||||
24
backend/internal/domain/context.go
Normal file
24
backend/internal/domain/context.go
Normal 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
|
||||
}
|
||||
13
backend/internal/domain/errors.go
Normal file
13
backend/internal/domain/errors.go
Normal 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")
|
||||
)
|
||||
54
backend/internal/domain/file.go
Normal file
54
backend/internal/domain/file.go
Normal 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
|
||||
}
|
||||
33
backend/internal/domain/pool.go
Normal file
33
backend/internal/domain/pool.go
Normal 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
|
||||
}
|
||||
33
backend/internal/domain/tag.go
Normal file
33
backend/internal/domain/tag.go
Normal 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
|
||||
}
|
||||
39
backend/internal/domain/user.go
Normal file
39
backend/internal/domain/user.go
Normal 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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user