From 83fda85bea9befa078782366d9ca0e7ebf701685 Mon Sep 17 00:00:00 2001 From: Masahiko AMANO Date: Sat, 4 Apr 2026 00:11:06 +0300 Subject: [PATCH] feat(backend): implement port interfaces (repository and storage) Define all repository interfaces in port/repository.go: FileRepo, TagRepo, TagRuleRepo, CategoryRepo, PoolRepo, UserRepo, SessionRepo, ACLRepo, AuditRepo, MimeRepo, and Transactor. Add OffsetParams and PoolFileListParams as shared parameter structs. Define FileStorage interface in port/storage.go with Save, Read, Delete, Thumbnail, and Preview methods. Co-Authored-By: Claude Sonnet 4.6 --- backend/internal/port/repository.go | 155 ++++++++++++++++++++++++++++ backend/internal/port/storage.go | 31 ++++++ 2 files changed, 186 insertions(+) create mode 100644 backend/internal/port/repository.go create mode 100644 backend/internal/port/storage.go diff --git a/backend/internal/port/repository.go b/backend/internal/port/repository.go new file mode 100644 index 0000000..d638c05 --- /dev/null +++ b/backend/internal/port/repository.go @@ -0,0 +1,155 @@ +package port + +import ( + "context" + "time" + + "github.com/google/uuid" + + "tanabata/backend/internal/domain" +) + +// Transactor executes fn inside a single database transaction. +// All repository calls made within fn receive the transaction via context. +type Transactor interface { + WithTx(ctx context.Context, fn func(ctx context.Context) error) error +} + +// OffsetParams holds common offset-pagination and sort parameters. +type OffsetParams struct { + Sort string + Order string // "asc" | "desc" + Search string + Offset int + Limit int +} + +// PoolFileListParams holds parameters for listing files inside a pool. +type PoolFileListParams struct { + Cursor string + Limit int + Filter string // filter DSL expression +} + +// FileRepo is the persistence interface for file records. +type FileRepo interface { + // List returns a cursor-based page of files. + List(ctx context.Context, params domain.FileListParams) (*domain.FilePage, error) + // GetByID returns the file with its tags loaded. + GetByID(ctx context.Context, id uuid.UUID) (*domain.File, error) + // Create inserts a new file record and returns it. + Create(ctx context.Context, f *domain.File) (*domain.File, error) + // Update applies partial metadata changes and returns the updated record. + Update(ctx context.Context, id uuid.UUID, f *domain.File) (*domain.File, error) + // SoftDelete moves a file to trash (sets is_deleted = true). + SoftDelete(ctx context.Context, id uuid.UUID) error + // Restore moves a file out of trash (sets is_deleted = false). + Restore(ctx context.Context, id uuid.UUID) (*domain.File, error) + // DeletePermanent removes a file record. Only allowed when is_deleted = true. + DeletePermanent(ctx context.Context, id uuid.UUID) error + + // ListTags returns all tags assigned to a file. + ListTags(ctx context.Context, fileID uuid.UUID) ([]domain.Tag, error) + // SetTags replaces all tags on a file (full replace semantics). + SetTags(ctx context.Context, fileID uuid.UUID, tagIDs []uuid.UUID) error +} + +// TagRepo is the persistence interface for tags. +type TagRepo interface { + List(ctx context.Context, params OffsetParams) (*domain.TagOffsetPage, error) + // ListByCategory returns tags belonging to a specific category. + ListByCategory(ctx context.Context, categoryID uuid.UUID, params OffsetParams) (*domain.TagOffsetPage, error) + GetByID(ctx context.Context, id uuid.UUID) (*domain.Tag, error) + Create(ctx context.Context, t *domain.Tag) (*domain.Tag, error) + Update(ctx context.Context, id uuid.UUID, t *domain.Tag) (*domain.Tag, error) + Delete(ctx context.Context, id uuid.UUID) error +} + +// TagRuleRepo is the persistence interface for auto-tag rules. +type TagRuleRepo interface { + // ListByTag returns all rules where WhenTagID == tagID. + ListByTag(ctx context.Context, tagID uuid.UUID) ([]domain.TagRule, error) + Create(ctx context.Context, r domain.TagRule) (*domain.TagRule, error) + // SetActive toggles a rule's is_active flag. + SetActive(ctx context.Context, whenTagID, thenTagID uuid.UUID, active bool) error + Delete(ctx context.Context, whenTagID, thenTagID uuid.UUID) error +} + +// CategoryRepo is the persistence interface for categories. +type CategoryRepo interface { + List(ctx context.Context, params OffsetParams) (*domain.CategoryOffsetPage, error) + GetByID(ctx context.Context, id uuid.UUID) (*domain.Category, error) + Create(ctx context.Context, c *domain.Category) (*domain.Category, error) + Update(ctx context.Context, id uuid.UUID, c *domain.Category) (*domain.Category, error) + Delete(ctx context.Context, id uuid.UUID) error +} + +// PoolRepo is the persistence interface for pools and pool–file membership. +type PoolRepo interface { + List(ctx context.Context, params OffsetParams) (*domain.PoolOffsetPage, error) + GetByID(ctx context.Context, id uuid.UUID) (*domain.Pool, error) + Create(ctx context.Context, p *domain.Pool) (*domain.Pool, error) + Update(ctx context.Context, id uuid.UUID, p *domain.Pool) (*domain.Pool, error) + Delete(ctx context.Context, id uuid.UUID) error + + // ListFiles returns pool files ordered by position (cursor-based). + ListFiles(ctx context.Context, poolID uuid.UUID, params PoolFileListParams) (*domain.PoolFilePage, error) + // AddFiles appends files starting at position; nil position means append at end. + AddFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID, position *int) error + // RemoveFiles removes files from the pool. + RemoveFiles(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error + // Reorder sets the full ordered sequence of file IDs in the pool. + Reorder(ctx context.Context, poolID uuid.UUID, fileIDs []uuid.UUID) error +} + +// UserRepo is the persistence interface for users. +type UserRepo interface { + List(ctx context.Context, params OffsetParams) (*domain.UserPage, error) + GetByID(ctx context.Context, id int16) (*domain.User, error) + // GetByName is used during login to look up credentials. + GetByName(ctx context.Context, name string) (*domain.User, error) + Create(ctx context.Context, u *domain.User) (*domain.User, error) + Update(ctx context.Context, id int16, u *domain.User) (*domain.User, error) + Delete(ctx context.Context, id int16) error +} + +// SessionRepo is the persistence interface for auth sessions. +type SessionRepo interface { + // ListByUser returns all active sessions for a user. + ListByUser(ctx context.Context, userID int16) (*domain.SessionList, error) + // GetByTokenHash looks up a session by the hashed refresh token. + GetByTokenHash(ctx context.Context, hash string) (*domain.Session, error) + Create(ctx context.Context, s *domain.Session) (*domain.Session, error) + // UpdateLastActivity refreshes the last_activity timestamp. + UpdateLastActivity(ctx context.Context, id int, t time.Time) error + // Delete terminates a single session. + Delete(ctx context.Context, id int) error + // DeleteByUserID terminates all sessions for a user (logout everywhere). + DeleteByUserID(ctx context.Context, userID int16) error +} + +// ACLRepo is the persistence interface for per-object permissions. +type ACLRepo interface { + // List returns all permission entries for a given object. + List(ctx context.Context, objectTypeID int16, objectID uuid.UUID) ([]domain.Permission, error) + // Get returns the permission entry for a specific user and object; returns + // ErrNotFound if no entry exists. + Get(ctx context.Context, userID int16, objectTypeID int16, objectID uuid.UUID) (*domain.Permission, error) + // Set replaces all permissions for an object (full replace semantics). + Set(ctx context.Context, objectTypeID int16, objectID uuid.UUID, perms []domain.Permission) error +} + +// AuditRepo is the persistence interface for the audit log. +type AuditRepo interface { + Log(ctx context.Context, entry domain.AuditEntry) error + List(ctx context.Context, filter domain.AuditFilter) (*domain.AuditPage, error) +} + +// MimeRepo is the persistence interface for the MIME type whitelist. +type MimeRepo interface { + // List returns all supported MIME types. + List(ctx context.Context) ([]domain.MIMEType, error) + // GetByName returns the MIME type record for a given MIME name (e.g. "image/jpeg"). + // Returns ErrUnsupportedMIME if not in the whitelist. + GetByName(ctx context.Context, name string) (*domain.MIMEType, error) +} diff --git a/backend/internal/port/storage.go b/backend/internal/port/storage.go new file mode 100644 index 0000000..d01629c --- /dev/null +++ b/backend/internal/port/storage.go @@ -0,0 +1,31 @@ +package port + +import ( + "context" + "io" + + "github.com/google/uuid" +) + +// FileStorage abstracts disk (or object-store) operations for file content, +// thumbnails, and previews. +type FileStorage interface { + // Save writes the reader's content to storage and returns the number of + // bytes written. ext is the file extension without a leading dot (e.g. "jpg"). + Save(ctx context.Context, id uuid.UUID, ext string, r io.Reader) (int64, error) + + // Read opens the file content for reading. The caller must close the returned + // ReadCloser. + Read(ctx context.Context, id uuid.UUID, ext string) (io.ReadCloser, error) + + // Delete removes the file content from storage. + Delete(ctx context.Context, id uuid.UUID, ext string) error + + // Thumbnail opens the pre-generated thumbnail (JPEG). Returns ErrNotFound + // if the thumbnail has not been generated yet. + Thumbnail(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) + + // Preview opens the pre-generated preview image (JPEG). Returns ErrNotFound + // if the preview has not been generated yet. + Preview(ctx context.Context, id uuid.UUID) (io.ReadCloser, error) +}