Files
mygo/docs/server/architecture.md
T
ld a18f96912d feat(repository): refactor query/mutation ports with capability-safe
interfaces

- refactor: split UserRepository into AuthUserRepository and
  AdminUserRepository capabilities
- refactor: split SessionRepository into AuthSessionRepository and
  repository-owned CLI/admin methods
- refactor: replace generic Repository Update/Delete with
  operation-specific params and ownership predicates
- refactor: replace CredentialRepository generic CRUD with
  passkey-specific methods
- refactor: replace FileRepository generic Create/Update/Delete with
  UploadedFileParams, DirectoryParams, and owned soft-delete
- refactor: remove repository fields from WebApp struct; repositories
  are now composition-time wiring only
- feat: add domain error kinds ErrParentNotFound, ErrParentNotDir,
  ErrDirectoryNotEmpty, ErrInvalidMove
- feat: add CredentialTypeAppPasskey constant
- feat: add testutil.SetUserAdmin for test fixture setup that bypasses
  production service ports
- test: add architecture test banning GORM Save in repository package
- test: add capability interface contract tests ensuring each service
  receives the minimal interface
- test: add blockingStorage helper for concurrent promotion tests
- test: add preserved DSN parameter test for sqliteImmediateDSN
- docs: update architecture decisions with repository write rules and
  capability separation
- docs: update roadmap to clarify atomic single-use refresh sessions
- docs: add -race test target to development docs
2026-07-16 12:24:36 +08:00

6.6 KiB

Architecture

Layered Design

Handler (Gin handlers)          ← translates HTTP ↔ Service calls
    ↓
Service (business logic)        ← orchestrates, authorizes, validates
    ↓                        ↓
Repository (GORM data access)   Storage (file I/O)
    ↓                        ↓
[SQLite / PostgreSQL]        [Local FS / S3]

Rules:

  • Handler has no business logic — parse request, call service, write response.
  • Handler and middleware never access repositories directly; they depend on services.
  • Service has no HTTP awareness — operates on domain models and interfaces.
  • internal/model and internal/service do not import net/http, Gin, or internal/api.
  • JSON response DTOs live in HTTP packages. Domain and service result structs are not the public REST schema.
  • Domain models may use json:"-" for defensive redaction of sensitive fields; this is not a REST response contract.
  • Domain errors carry a protocol-neutral kind, a safe message, and an optional cause. HTTP status mapping happens only at the API boundary.
  • Repository query ports remain entity-oriented, while mutation ports expose business commands with operation-specific parameter types. Services never pass a mutable domain entity to a generic update or delete method.
  • Service constructors using split repository capabilities accept the smallest capability needed by that service. Their composition-time unions do not cross into business services.
  • Repository mutations use explicit columns and lifecycle/ownership predicates. Full-entity Save is prohibited so a write cannot accidentally change protected fields or revive a soft-deleted record.
  • Repository abstracts the database; Storage abstracts where bytes live, including staged upload promotion.
  • internal/server is the composition root — wires all dependencies together.

Package Map

Layer Package Purpose Status
CLI cmd Cobra root command 🛠 WIP
cmd/serve.go mygo serve — wire deps, start HTTP
cmd/config.go mygo config — config subcommand 🛠 WIP
cmd/status.go mygo status — health check 🛠 WIP
Config internal/config Viper load (YAML + env + flags), typed Duration config via built-in decode hook
App internal/app Runtime dependency container and build metadata 🛠 WIP
HTTP internal/server Gin router init, route registration (public/protected split), graceful shutdown
internal/handler HTTP handlers (auth, file, admin, webdav...) 🛠 WIP
internal/middleware Gin middleware (logger, jwt, cors, auth) 🛠 WIP
Business internal/service Business logic: AuthService, FileService, AdminService; protocol-neutral results and errors
internal/model Domain types (User, File, Credential, Session), protocol-neutral error kinds
Data internal/repository Repository interfaces + GORM implementations (User, Session, File, Credential)
internal/storage Storage backend interface + local disk impl, with staging and promotion 🛠 WIP
Util internal/auth JWT sign/verify (HS256), token type discrimination (access/refresh), password hashing (bcrypt), app passkey tokens
internal/api Unified JSON error response helpers and REST error-kind mapping
internal/testutil Test-only fixture helpers for privileged setup that production service ports intentionally do not expose

Persistence Write Boundaries

  • User registration accepts RegisteredUserParams; the repository fixes new users to non-admin, active state. Administrative deletion is a separate capability.
  • File creation, directory creation, metadata changes, and soft deletion use distinct commands. Metadata updates can only write name, parent_id, and GORM-maintained updated_at.
  • Refresh sessions are created, atomically consumed, idempotently revoked, or removed by explicit maintenance operations. Refresh token rotation consumes a session at most once.
  • Passkey creation fixes the credential type to the app-passkey type. Usage recording and revocation include credential-type and ownership conditions where applicable.
  • app.WebApp contains runtime services, storage, database lifecycle, configuration, and metadata. Repository implementations remain local to bootstrap and are injected directly into the services that require them.

File Hierarchy Transactions

File child creation, movement, and directory deletion share one parent-locking protocol. PostgreSQL locks active owned source and parent rows in sorted ID order with SELECT ... FOR UPDATE; directory deletion checks active children while holding the directory lock. SQLite connections add _txlock=immediate to the configured DSN so the transaction acquires its write reservation before reading hierarchy state. These rules prevent a concurrent mutation from creating an active child below a deleted directory.

API Routes (v0)

GET /api/v1/version

POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout

GET    /api/v1/account
GET    /api/v1/account/passkeys
POST   /api/v1/account/passkeys
DELETE /api/v1/account/passkeys/:id

GET    /api/v1/files
POST   /api/v1/files                   # JSON creates directories; multipart uploads files. File upload parent_id is a query parameter.
GET    /api/v1/files/:id
GET    /api/v1/files/:id/content
PUT    /api/v1/files/:id
DELETE /api/v1/files/:id

GET    /api/v1/admin/users
GET    /api/v1/admin/users/:id
PUT    /api/v1/admin/users/:id
DELETE /api/v1/admin/users/:id

Middleware Chain

Applied globally by gin.Default(): logger → recovery

Planned globally: cors

Applied to protected groups: auth (extract bearer token, delegate access-token authentication to AuthService, inject principal into gin.Context)

Applied to admin groups: admin (check authenticated principal from context)

Server Responsibilities

  • cmd/serve.go loads config, calls app.Bootstrap to initialize DB + services, builds the router, and starts the HTTP server.
  • app.WebApp carries runtime dependencies and build metadata needed to assemble handlers.
  • internal/server owns Gin router setup (router.go), route registration split into routes_public.go (public auth) and routes_protected.go (JWT-protected account).
  • Each route group creates its own handler instance: routes_public.go creates AuthHandler, routes_protected.go creates AccountHandler — no shared handler state between public and protected routes.
  • RunWithGracefulShutdown stops accepting new requests on termination and gives in-flight requests time to finish.