diff --git a/docs/superpowers/specs/2026-07-28-ddd-clean-architecture-restructure.md b/docs/superpowers/specs/2026-07-28-ddd-clean-architecture-restructure.md new file mode 100644 index 0000000..5f106f7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-ddd-clean-architecture-restructure.md @@ -0,0 +1,310 @@ +# DDD & Clean Architecture Restructure — TeleUploader (filedrop) + +**Date:** 2026-07-28 +**Author:** Claude +**Status:** Draft + +## 1. Motivation + +The current codebase uses a flat structure (`src/routes/`, `src/utils/`, `src/db/`) that mixes concerns: + +- Route handlers contain inline DB queries, Telegram API calls, and business logic +- Domain entity types (from Drizzle ORM) leak into all layers +- No clear separation between HTTP concerns, application logic, and infrastructure +- Large files (`routes/s3.ts` at 1179 lines) are hard to maintain, test, and reason about + +The goal is to restructure into **Domain-Driven Design (DDD)** with **Clean Architecture** principles, adding **JSDoc documentation** throughout, without changing behaviour or breaking existing tests. + +## 2. Approach: Hybrid C + +| Aspect | Decision | +|--------|----------| +| **Depth** | Structural DDD + repository abstraction + controller/use-case separation | +| **Domain entities** | Re-use Drizzle-generated types (pragmatic, not creating new pure domain models) | +| **Dependency inversion** | Repository interfaces defined in `domain/ports/`, implementations in `infrastructure/` | +| **Dependency injection** | Simple factory functions (no DI framework) | +| **Testing** | Test files restructured alongside source files | +| **Documentation** | JSDoc on all public functions, classes, interfaces, and types | +| **Language** | English for code and JSDoc | + +## 3. Layer Architecture + +``` +┌─────────────────────────────────────────────┐ +│ interfaces/ (Delivery) │ +│ HTTP controllers, routes, middleware │ +│ Bot handlers │ +│ S3 protocol (SigV4, XML, range, stream) │ +├─────────────────────────────────────────────┤ +│ application/ (Use Cases) │ +│ Upload file, get file, auth, bucket ops, │ +│ S3 object ops, multipart upload │ +├────────────────────┬────────────────────────┤ +│ domain/ │ infrastructure/ │ +│ entities/ │ persistence/ │ +│ ports/ (interfaces)│ telegram/ │ +│ │ cache/ │ +├────────────────────┴────────────────────────┤ +│ config/ + shared/ │ +└─────────────────────────────────────────────┘ +``` + +**Dependency Rule:** Outer layers depend on inner layers, never the reverse. Domain knows nothing about HTTP, Drizzle, or Telegram. + +## 4. Full Directory Structure + +``` +src/ +├── index.ts # Entry point: server bootstrap, lifecycle +├── domain/ +│ ├── entities/ +│ │ ├── file.ts # File entity type (re-export from drizzle) +│ │ ├── bucket.ts # Bucket entity type +│ │ ├── file-part.ts # FilePart entity type +│ │ └── multipart.ts # MultipartUpload, MultipartPart types +│ └── ports/ +│ ├── file-repository.ts # IFileRepository interface +│ ├── bucket-repository.ts # IBucketRepository interface +│ ├── file-part-repository.ts # IFilePartRepository interface +│ └── multipart-repository.ts # IMultipartRepository interface +├── application/ +│ ├── dto/ +│ │ ├── upload.ts # Upload request/response DTOs +│ │ ├── file.ts # File response DTOs +│ │ ├── bucket.ts # Bucket DTOs +│ │ ├── s3.ts # S3 operation DTOs +│ │ └── auth.ts # Auth DTOs +│ └── use-cases/ +│ ├── upload-file.ts # Upload logic (multipart, JSON base64) +│ ├── get-file.ts # File redirect/stream logic +│ ├── manage-bucket.ts # Bucket CRUD logic +│ ├── s3-object.ts # S3 get/put/delete/copy logic +│ ├── multipart-upload.ts # S3 multipart upload logic +│ └── authenticate.ts # Login/logout/session logic +├── infrastructure/ +│ ├── persistence/ +│ │ ├── drizzle/ +│ │ │ ├── index.ts # DB client + Drizzle init (from db/index.ts) +│ │ │ └── schema.ts # Drizzle table definitions (from db/schema.ts) +│ │ │ └── migrate.ts # Migration runner (from db/migrate.ts) +│ │ └── repositories/ +│ │ ├── file-repository.ts # IFileRepository implementation (Drizzle) +│ │ ├── bucket-repository.ts # IBucketRepository implementation +│ │ ├── file-part-repository.ts# IFilePartRepository implementation +│ │ └── multipart-repository.ts# IMultipartRepository implementation +│ ├── telegram/ +│ │ ├── bot-pool.ts # Multi-bot pool + retry (from utils/telegram.ts) +│ │ ├── upload-queue.ts # P-Queue wrapper (from utils/telegramQueue.ts) +│ │ ├── upload-batcher.ts # Batch upload (from utils/uploadBatcher.ts) +│ │ ├── chunked-storage.ts # Chunked file storage (from utils/chunked-storage.ts) +│ │ └── types.ts # Telegram-related types +│ └── cache/ +│ └── index.ts # Generic TTL cache (from utils/cache.ts) +├── interfaces/ +│ ├── http/ +│ │ ├── controllers/ +│ │ │ ├── upload-controller.ts # Upload HTTP handler +│ │ │ ├── file-controller.ts # File redirect + info handler +│ │ │ ├── auth-controller.ts # Login/logout/me handler +│ │ │ ├── home-controller.ts # Home dashboard handler +│ │ │ ├── health-controller.ts # Health check handler +│ │ │ ├── s3-controller.ts # S3-compatible API handler +│ │ │ └── web-api-controller.ts # JSON REST API handler +│ │ ├── middleware/ +│ │ │ ├── auth.ts # requireAuth, getAuthSession +│ │ │ └── rate-limit.ts # Rate limiter +│ │ └── routes/ +│ │ └── index.ts # All route definitions +│ ├── bot/ +│ │ └── handler.ts # Telegram bot event handlers (from bot.ts) +│ └── s3/ +│ ├── auth.ts # SigV4 verification (from utils/s3/auth.ts) +│ ├── headers.ts # CORS/response headers +│ ├── object-stream.ts # Part streaming for GetObject +│ ├── range.ts # HTTP Range header parser +│ ├── virtual-host.ts # Virtual-hosted bucket extraction +│ └── xml.ts # S3 XML response builders/parsers +├── config/ +│ └── index.ts # Environment config (from env.ts) +├── shared/ +│ ├── errors/ +│ │ └── index.ts # Domain + application error classes +│ ├── logger/ +│ │ └── index.ts # Winston logger (from utils/logger.ts) +│ ├── metrics/ +│ │ └── index.ts # Metrics collector (from utils/metrics.ts) +│ └── utils/ +│ ├── file.ts # File type, MIME, hash, extension (from utils/file.ts) +│ ├── ip.ts # Client IP extraction (from utils/ip.ts) +│ ├── retry.ts # Retry/timeout/fallback (from utils/retry.ts) +│ └── zip.ts # ZIP create/extract (from utils/zip.ts) +``` + +## 5. Dependency Injection Pattern + +No DI framework. Use-case factories accept repository interfaces: + +```typescript +// domain/ports/file-repository.ts +export interface IFileRepository { + findByHash(hash: string): Promise; + findByPublicId(publicId: string): Promise; + findByUniqueId(uniqueId: string): Promise; + findByBucketAndKey(bucketId: string, key: string): Promise; + create(file: NewFile): Promise; + softDelete(bucketId: string, key: string): Promise; + listByPrefix(...): Promise<{ objects: File[]; prefixes: string[] }>; +} + +// application/use-cases/upload-file.ts +export function createUploadFileUseCase(repos: { + fileRepo: IFileRepository; + filePartRepo: IFilePartRepository; + telegramService: ITelegramService; + config: AppConfig; +}) { + return async (input: UploadInput): Promise => { + // business logic here, using repos.xxx() not db directly + }; +} +``` + +This keeps use cases testable — inject mock repositories in tests. + +## 6. Controller / Use-Case Boundary + +**Controller responsibilities:** +- Parse HTTP request (body, params, headers) +- Call use case +- Format HTTP response + +**Use case responsibilities:** +- Business logic (file size checks, dedup, chunking decisions) +- Orchestrate infrastructure calls (Telegram, DB, cache) +- Return DTOs + +**Example flow for upload:** +``` +UploadController.handleUpload(req) + → parse multipart form data + → write to temp file + → call uploadFileUseCase({ tempPath, fileName, mimeType, ... }) + → check dedup (fileRepo.findByHash) + → check file size limits + → if chunked: telegramService.uploadChunks(...), filePartRepo.create(...) + → if batch: telegramService.enqueueBatch(...) + → fileRepo.create(...) + → return UploadOutput + → format response (buildUploadResponse) + → return Response +``` + +## 7. Migration Strategy + +The restructure will be done in one pass per layer, with tests updated in lockstep: + +1. Create new folder structure +2. Move `shared/` utilities first (no dependencies on other layers) +3. Move `config/` +4. Move `domain/` entities + ports +5. Move `infrastructure/` persistence + telegram + cache +6. Move `application/` use cases + DTOs +7. Move `interfaces/` controllers + middleware + routes +8. Update `src/index.ts` entry point +9. Restructure test files matching new layout +10. Run all tests, fix import paths +11. Delete old folders + +## 8. Entity Architecture + +``` +┌──────────────────────────────────────────────┐ +│ Domain Layer │ +│ Entities: File, Bucket, FilePart, │ +│ MultipartUpload, MultipartPart │ +│ Ports: IFileRepository, IBucketRepository, │ +│ IFilePartRepository, │ +│ IMultipartRepository │ +├──────────────────────────────────────────────┤ +│ Application Layer │ +│ UploadFileUseCase │ +│ ├── depends on IFileRepository │ +│ ├── depends on IFilePartRepository │ +│ └── depends on ITelegramService │ +│ │ +│ S3ObjectUseCase │ +│ ├── depends on IFileRepository │ +│ ├── depends on IBucketRepository │ +│ ├── depends on IMultipartRepository │ +│ └── depends on ITelegramService │ +└──────────────────────────────────────────────┘ +``` + +## 9. Telegram Service Interface + +A new `ITelegramService` interface in `domain/ports/` to abstract Telegram operations: + +```typescript +export interface ITelegramService { + forwardToStorage(fileChunk: unknown, fileName: string, fileType: string): Promise; + getFileInfo(telegramFileId: string): Promise; + enqueueUpload(task: () => Promise): Promise; +} +``` + +Implementation stays in `infrastructure/telegram/`. + +## 10. JSDoc Standards + +Every exported function, interface, type, and class gets JSDoc: + +```typescript +/** + * Uploads a file to Telegram storage, handling chunking for large files + * and batch deduplication for small files. + * + * @param input - The prepared upload data and metadata + * @param input.tempPath - Path to the temporary file on disk + * @param input.fileName - Display name for the file + * @param input.mimeType - MIME type of the file + * @param input.fileType - Telegram file category (document, photo, etc.) + * @param input.sizeBytes - File size in bytes + * @returns The stored file entity with Telegram metadata + * @throws {FileTooLargeError} If file exceeds Telegram size limits + */ +``` + +## 11. Error Handling + +Custom error classes replacing generic `Error` throws: + +```typescript +// shared/errors/index.ts +export class DomainError extends Error { constructor(msg: string) { super(msg); this.name = 'DomainError'; } } +export class FileNotFoundError extends DomainError {} +export class BucketNotFoundError extends DomainError {} +export class FileTooLargeError extends DomainError {} +export class DuplicateFileError extends DomainError {} +export class AuthenticationError extends DomainError {} +export class ValidationError extends DomainError {} +``` + +Use cases throw domain errors. Controllers catch and format HTTP responses. + +## 12. Existing Files That Remain Unchanged + +Files that are purely structural (no business logic restructuring needed): + +| Old Path | New Path | +|----------|----------| +| `src/home.html` | `src/interfaces/http/controllers/home.html` (or alongside home controller) | +| `src/utils/s3/*` | `src/interfaces/s3/*` (moved as-is) | +| `src/db/migrate.ts` | `src/infrastructure/persistence/drizzle/migrate.ts` | + +## 13. State After Migration + +- **Total source files:** ~60 (was ~30) — more focused files, each with clear responsibility +- **JSDoc coverage:** 100% of exported APIs +- **Test files:** ~24, restructured to mirror source layout +- **Behaviour changes:** Zero. No logic is altered, only moved +- **External interfaces:** All API endpoints, S3 XML formats, and response shapes unchanged