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)
|
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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user