From af8949de02fbc5e5ba17d0557a6d653298f88488 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 17:31:11 +0700 Subject: [PATCH] docs: add DDD/clean architecture restructure implementation plan Co-Authored-By: Claude Opus 5 (1M context) --- ...7-28-ddd-clean-architecture-restructure.md | 950 ++++++++++++++++++ 1 file changed, 950 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md diff --git a/docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md b/docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md new file mode 100644 index 0000000..2001549 --- /dev/null +++ b/docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md @@ -0,0 +1,950 @@ +# DDD & Clean Architecture Restructure Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restructure TeleUploader from flat architecture to DDD/Clean Architecture with repository abstraction, controller/use-case separation, and full JSDoc documentation. + +**Architecture:** 5-layer DDD — domain (entities + ports), application (use cases + DTOs), infrastructure (persistence + telegram + cache), interfaces (HTTP controllers + routes + middleware + bot + S3 protocol), and shared/config foundation. + +**Tech Stack:** Bun + TypeScript + Drizzle ORM + Telegraf + +## Global Constraints + +- Zero behavioral changes — only move/restructure code, never alter logic +- Old source files remain until Phase 8 (cleanup), so existing tests keep passing +- JSDoc on every exported function, interface, type, and class (English) +- All new imports use relative paths within new structure +- `config/` and `shared/` have zero dependencies on other new layers +- Domain `ports/` interfaces are implemented by `infrastructure/` repositories +- Use cases constructed via factory functions accepting repository interfaces (no DI framework) +- Controller = HTTP parsing + use case call + response formatting only; no business logic +- Domain entities reuse Drizzle types (no pure domain models) +- `bun test test/` must pass at every phase (old files remain) + +--- + +### Phase 1: Foundation Layer — Config + Shared Utilities + +#### Task 1: Create config/index.ts + +**Files:** +- Create: `src/config/index.ts` + +**Interfaces:** +- Produces: `AppConfig` type, `config: AppConfig` singleton + +- [ ] **Create `src/config/index.ts`** — Move content from `src/env.ts`. Same `AppConfig` interface, same `config` export. Add JSDoc to `AppConfig` interface (document every property), the `config` export, `parseNumber`, `parseTokens`, `parseDomains`, `maskSecret`, `maskDatabaseUrl`. + +- [ ] **Run tests to verify nothing broken** + +```bash +bun test test/env.test.ts +``` +Expected: PASS + +- [ ] **Create `src/config/__tests__/env.test.ts`** — Mirror of `test/env.test.ts` but imports from `../../config/index.ts` + +- [ ] **Run both env tests to verify** + +```bash +bun test test/env.test.ts +bun test src/config/__tests__/env.test.ts +``` +Expected: Both PASS + +- [ ] **Commit** + +```bash +git add src/config/ +git commit -m "feat: create config layer with JSDoc" +``` + +#### Task 2: Create shared/ errors and logger + +**Files:** +- Create: `src/shared/errors/index.ts` +- Create: `src/shared/logger/index.ts` + +**Interfaces:** +- Produces: `DomainError`, `FileNotFoundError`, `BucketNotFoundError`, `FileTooLargeError`, `DuplicateFileError`, `AuthenticationError`, `ValidationError`, `logger` singleton, type `Logger` + +- [ ] **Create `src/shared/errors/index.ts`** — Export all error classes from spec Section 11. Add JSDoc for each class. + +- [ ] **Create `src/shared/logger/index.ts`** — Move content from `src/utils/logger.ts`. Same implementation. Add JSDoc. + +- [ ] **Quick compile check** + +```bash +bun build src/config/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +bun build src/shared/errors/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/shared/errors/ src/shared/logger/ +git commit -m "feat: create shared errors and logger layer" +``` + +#### Task 3: Create shared/ metrics + +**Files:** +- Create: `src/shared/metrics/index.ts` + +**Interfaces:** +- Produces: `metricsCollector`, `MetricsCollector`, `getSnapshot()` + +- [ ] **Create `src/shared/metrics/index.ts`** — Move content from `src/utils/metrics.ts`. Same implementation. Add JSDoc to class, all methods, and `getSnapshot()` return type. + +- [ ] **Compile check** + +```bash +bun build src/shared/metrics/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/shared/metrics/ +git commit -m "feat: create shared metrics layer" +``` + +#### Task 4: Create shared/utils (file, ip, retry, zip) + +**Files:** +- Create: `src/shared/utils/file.ts` +- Create: `src/shared/utils/ip.ts` +- Create: `src/shared/utils/retry.ts` +- Create: `src/shared/utils/zip.ts` + +**Interfaces:** +- Produces: `getErrorMessage`, `cleanupTempFile`, `getFileType`, `checkFileSize`, `ensureExtension`, `extractFileName`, `extractMimeType`, `computeHash`, `extractFileFromMessage`, `detectFileType`, `getFileSizeLimit`, `formatCreatedAt`, `buildUploadResponse`, `extractClientIp`, `withRetry`, `withTimeout`, `withFallback`, `createZip`, `extractZipEntry`, `locateZipEntry` + +- [ ] **Create `src/shared/utils/file.ts`** — Move all exports from `src/utils/file.ts`. Add JSDoc to every export. Keep all logic identical. + +- [ ] **Create `src/shared/utils/ip.ts`** — Move `extractClientIp` from `src/utils/ip.ts`. Add JSDoc. + +- [ ] **Create `src/shared/utils/retry.ts`** — Move all exports from `src/utils/retry.ts`. Add JSDoc. + +- [ ] **Create `src/shared/utils/zip.ts`** — Move all exports from `src/utils/zip.ts`. Add JSDoc. + +- [ ] **Run existing tests to verify** + +```bash +bun test test/rateLimit.test.ts +bun test test/file.test.ts +bun test test/zip.test.ts +``` +Expected: PASS + +- [ ] **Commit** + +```bash +git add src/shared/utils/ +git commit -m "feat: create shared utilities layer with JSDoc" +``` + +--- + +### Phase 2: Domain Layer — Entities + Ports + +#### Task 5: Create domain/entities (File, Bucket, FilePart, Multipart) + +**Files:** +- Create: `src/domain/entities/file.ts` +- Create: `src/domain/entities/bucket.ts` +- Create: `src/domain/entities/file-part.ts` +- Create: `src/domain/entities/multipart.ts` + +**Interfaces:** +- Produces: `File`, `NewFile`, `FilePart`, `NewFilePart`, `Bucket`, `MultipartUpload`, `MultipartPart` types + +- [ ] **Create `src/domain/entities/file.ts`** — Re-export `File` and `NewFile` types from Drizzle schema. Add domain JSDoc. + +```typescript +// Re-export Drizzle types as domain entities +export type { File, NewFile } from '../../infrastructure/persistence/drizzle/schema'; +``` + +Wait — this creates a circular dependency! Domain shouldn't import from infrastructure. Instead, define standalone interfaces: + +```typescript +/** + * Core domain entity representing a file stored in Telegram. + * Contains both Telegram metadata and optional S3-compatible fields. + */ +export interface File { + id: string; + publicId: string; + telegramFileId: string; + telegramFileUniqueId: string; + storageChatId: number; + storageMessageId: number; + fileName: string; + mimeType: string; + sizeBytes: number; + fileType: string; + uploaderId: number; + fileHash: string | null; + archiveTelegramFileId: string | null; + archiveStorageMessageId: number | null; + archiveFileName: string | null; + archiveEntryName: string | null; + archiveMimeType: string | null; + archiveSizeBytes: number | null; + bucketId: string | null; + s3Key: string | null; + storageBackend: string | null; + isDeleted: boolean | null; + multipartUploadId: string | null; + partCount: number | null; + createdAt: Date; + updatedAt: Date; +} + +export type NewFile = Omit; +``` + +- [ ] **Create `src/domain/entities/bucket.ts`** — Standalone Bucket interface. + +- [ ] **Create `src/domain/entities/file-part.ts`** — Standalone FilePart + NewFilePart types with `CompressionAlgorithm`. + +- [ ] **Create `src/domain/entities/multipart.ts`** — Standalone MultipartUpload, MultipartPart types. + +- [ ] **Compile check** + +```bash +bun build src/domain/entities/file.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/domain/entities/ +git commit -m "feat: create domain entities with JSDoc" +``` + +#### Task 6: Create domain/ports (repository interfaces) + +**Files:** +- Create: `src/domain/ports/file-repository.ts` +- Create: `src/domain/ports/bucket-repository.ts` +- Create: `src/domain/ports/file-part-repository.ts` +- Create: `src/domain/ports/multipart-repository.ts` + +**Interfaces:** +- Produces: `IFileRepository`, `IBucketRepository`, `IFilePartRepository`, `IMultipartRepository`, `ITelegramService`, `S3FileRecord`, `CompressionAlgorithm` + +- [ ] **Create `src/domain/ports/file-repository.ts`** — Define interface for all file operations currently in `db/files.ts` and `db/files-ext.ts`. + +```typescript +import type { File, NewFile } from '../entities/file'; + +export interface S3FileRecord extends File { + bucketId: string; + s3Key: string; +} + +export interface IFileRepository { + findByHash(hash: string): Promise; + findByPublicId(publicId: string): Promise; + findByUniqueId(telegramFileUniqueId: string): Promise; + findByBucketAndKey(bucketId: string, s3Key: string): Promise; + create(file: NewFile): Promise; + listByPrefix( + bucketId: string, + prefix: string, + delimiter: string | null, + maxKeys: number, + startAfter: string | null, + ): Promise<{ objects: S3FileRecord[]; prefixes: string[] }>; + softDelete(bucketId: string, s3Key: string): Promise; + softDeleteBatch(bucketId: string, keys: string[]): Promise; + countByBucket(bucketId: string): Promise; + findOrphansByBucket(bucketId: string): Promise; +} +``` + +- [ ] **Create `src/domain/ports/bucket-repository.ts`** — Interface for bucket CRUD. + +- [ ] **Create `src/domain/ports/file-part-repository.ts`** — Interface for file parts operations. + +- [ ] **Create `src/domain/ports/multipart-repository.ts`** — Interface for multipart upload operations. + +- [ ] **Create `src/domain/ports/telegram-service.ts`** — Interface for Telegram operations. + +```typescript +/** Result of forwarding a file to Telegram storage */ +export interface ForwardResult { + telegramFileId: string; + telegramFileUniqueId: string; + storageMessageId: number; +} + +/** File information returned by Telegram's getFile API */ +export interface TelegramFileInfo { + file_size: number; + mime_type: string; + file_path: string; + bot_token: string; +} + +/** Abstraction over Telegram bot API operations */ +export interface ITelegramService { + forwardToStorage(fileChunk: unknown, fileName: string, fileType: string): Promise; + getFileInfo(telegramFileId: string): Promise; + enqueueUpload(task: () => Promise): Promise; +} +``` + +- [ ] **Compile check** + +```bash +bun build src/domain/ports/file-repository.ts --target=bun --outfile=/dev/null 2>&1 +bun build src/domain/ports/telegram-service.ts --target=bun --outfile=/dev/null 2>&1 +``` +Expected: No errors (domain has no infrastructure dependencies) + +- [ ] **Commit** + +```bash +git add src/domain/ports/ +git commit -m "feat: create domain port interfaces with JSDoc" +``` + +--- + +### Phase 3: Infrastructure Layer — Persistence, Telegram, Cache + +#### Task 7: Create infrastructure/persistence/drizzle (DB init, schema, migrate) + +**Files:** +- Create: `src/infrastructure/persistence/drizzle/index.ts` +- Create: `src/infrastructure/persistence/drizzle/schema.ts` +- Create: `src/infrastructure/persistence/drizzle/migrate.ts` + +**Interfaces:** +- Consumes: domain entities (for type alignment) +- Produces: `db` client, `files`, `fileParts` table definitions, `runMigration()`, Drizzle schema types + +- [ ] **Create `src/infrastructure/persistence/drizzle/index.ts`** — Move content from `src/db/index.ts`. Same implementation. Add JSDoc to exports. + +- [ ] **Create `src/infrastructure/persistence/drizzle/schema.ts`** — Move content from `src/db/schema.ts`. Same implementation. Add JSDoc to table definitions and exported types. + +- [ ] **Create `src/infrastructure/persistence/drizzle/migrate.ts`** — Move content from `src/db/migrate.ts`. Same implementation. Add JSDoc. + +- [ ] **Compile check** + +```bash +bun build src/infrastructure/persistence/drizzle/index.ts --target=bun --outfile=/dev/null 2>&1 | head -10 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/infrastructure/persistence/drizzle/ +git commit -m "feat: create persistence drizzle layer" +``` + +#### Task 8: Create infrastructure/persistence/repositories (implement ports) + +**Files:** +- Create: `src/infrastructure/persistence/repositories/file-repository.ts` +- Create: `src/infrastructure/persistence/repositories/bucket-repository.ts` +- Create: `src/infrastructure/persistence/repositories/file-part-repository.ts` +- Create: `src/infrastructure/persistence/repositories/multipart-repository.ts` + +**Interfaces:** +- Consumes: `IFileRepository`, `IBucketRepository`, `IFilePartRepository`, `IMultipartRepository` from domain/ports +- Produces: Concrete repository classes implementing each interface + +- [ ] **Create `src/infrastructure/persistence/repositories/file-repository.ts`** — Implement `IFileRepository` using Drizzle. Extract logic from `src/db/files.ts` and `src/db/files-ext.ts`. Methods delegate to Drizzle queries (same SQL as original). + +```typescript +import { eq, and, sql } from 'drizzle-orm'; +import { db, files as fileSchema } from '../drizzle/index'; +import type { File, NewFile } from '../../../domain/entities/file'; +import type { IFileRepository, S3FileRecord } from '../../../domain/ports/file-repository'; + +export class DrizzleFileRepository implements IFileRepository { + async findByHash(hash: string): Promise { + const result = await db.select().from(fileSchema).where(eq(fileSchema.fileHash, hash)).limit(1); + return result[0] || null; + } + // ... all methods from original files.ts + files-ext.ts +} +``` + +- [ ] **Create `src/infrastructure/persistence/repositories/bucket-repository.ts`** — Implement `IBucketRepository` from `src/db/buckets.ts`. + +- [ ] **Create `src/infrastructure/persistence/repositories/file-part-repository.ts`** — Implement `IFilePartRepository` from `src/db/file-parts.ts`. + +- [ ] **Create `src/infrastructure/persistence/repositories/multipart-repository.ts`** — Implement `IMultipartRepository` from `src/db/multipart.ts`. + +- [ ] **Compile check** + +```bash +bun build src/infrastructure/persistence/repositories/file-repository.ts --target=bun --outfile=/dev/null 2>&1 | head -10 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/infrastructure/persistence/repositories/ +git commit -m "feat: create Drizzle repository implementations" +``` + +#### Task 9: Create infrastructure/telegram (bot-pool, queue, batcher, chunked-storage) + +**Files:** +- Create: `src/infrastructure/telegram/types.ts` +- Create: `src/infrastructure/telegram/bot-pool.ts` +- Create: `src/infrastructure/telegram/upload-queue.ts` +- Create: `src/infrastructure/telegram/upload-batcher.ts` +- Create: `src/infrastructure/telegram/chunked-storage.ts` + +**Interfaces:** +- Consumes: `ITelegramService`, `IFilePartRepository`, `IFileRepository` from domain/ports +- Produces: `DrizzleFileRepository`, `BotPool`, etc. + +- [ ] **Create `src/infrastructure/telegram/types.ts`** — Extract Telegram-specific types from `src/utils/telegram.ts` (no longer inline). + +- [ ] **Create `src/infrastructure/telegram/bot-pool.ts`** — Move multi-bot pool + retry logic from `src/utils/telegram.ts`. Implement `ITelegramService` interface. Add JSDoc. + +- [ ] **Create `src/infrastructure/telegram/upload-queue.ts`** — Move P-Queue wrapper from `src/utils/telegramQueue.ts`. Add JSDoc. + +- [ ] **Create `src/infrastructure/telegram/upload-batcher.ts`** — Move batch upload logic from `src/utils/uploadBatcher.ts`. Add JSDoc. Accept repository interface instead of importing `db` directly. + +- [ ] **Create `src/infrastructure/telegram/chunked-storage.ts`** — Move chunked storage logic from `src/utils/chunked-storage.ts`. Accept repository interfaces. Add JSDoc. + +- [ ] **Compile check** + +```bash +bun build src/infrastructure/telegram/bot-pool.ts --target=bun --outfile=/dev/null 2>&1 | head -10 +bun build src/infrastructure/telegram/upload-batcher.ts --target=bun --outfile=/dev/null 2>&1 | head -10 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/infrastructure/telegram/ +git commit -m "feat: create infrastructure telegram layer" +``` + +#### Task 10: Create infrastructure/cache + +**Files:** +- Create: `src/infrastructure/cache/index.ts` + +**Interfaces:** +- Produces: `Cache`, `fileInfoCache` singleton + +- [ ] **Create `src/infrastructure/cache/index.ts`** — Move generic TTL cache from `src/utils/cache.ts`. Add JSDoc. Same implementation. + +- [ ] **Compile check** + +```bash +bun build src/infrastructure/cache/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/infrastructure/cache/ +git commit -m "feat: create infrastructure cache layer" +``` + +--- + +### Phase 4: Application Layer — DTOs + Use Cases + +#### Task 11: Create application/dto + +**Files:** +- Create: `src/application/dto/upload.ts` +- Create: `src/application/dto/file.ts` +- Create: `src/application/dto/bucket.ts` +- Create: `src/application/dto/s3.ts` +- Create: `src/application/dto/auth.ts` + +**Interfaces:** +- Produces: `UploadInput`, `UploadOutput`, `FileInfoResponse`, `BucketResponse`, `S3ObjectResponse`, `AuthSession`, etc. + +- [ ] **Create `src/application/dto/upload.ts`** — Define upload request/response DTOs. + +```typescript +/** Input for the upload file use case */ +export interface UploadInput { + tempPath: string; + fileHash: string; + fileName: string; + mimeType: string; + fileType: string; + sizeBytes: number; + uploaderId?: number; + bucketId?: string | null; + s3Key?: string | null; +} + +/** Output from the upload file use case */ +export interface UploadOutput { + publicId: string; + fileName: string; + mimeType: string; + sizeBytes: number; + fileType: string; + createdAt: Date; + downloadUrl: string; +} +``` + +- [ ] **Create `src/application/dto/file.ts`** — File info response DTOs. + +- [ ] **Create `src/application/dto/bucket.ts`** — Bucket CRUD DTOs. + +- [ ] **Create `src/application/dto/s3.ts`** — S3 operation DTOs (list, copy, multipart). + +- [ ] **Create `src/application/dto/auth.ts`** — Auth request/response DTOs (login input, session info). + +- [ ] **Compile check** + +```bash +bun build src/application/dto/upload.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/application/dto/ +git commit -m "feat: create application DTOs" +``` + +#### Task 12: Create application/use-cases (upload, get-file, auth) + +**Files:** +- Create: `src/application/use-cases/upload-file.ts` +- Create: `src/application/use-cases/get-file.ts` +- Create: `src/application/use-cases/authenticate.ts` + +**Interfaces:** +- Consumes: Repository interfaces from domain/ports, DTOs from application/dto +- Produces: Factory functions returning use case closures + +- [ ] **Create `src/application/use-cases/upload-file.ts`** — Extract upload business logic from `src/routes/upload.ts` + `src/utils/chunked-storage.ts`. Factory function pattern: + +```typescript +import { nanoid } from 'nanoid'; +import type { IFileRepository } from '../../domain/ports/file-repository'; +import type { IFilePartRepository } from '../../domain/ports/file-part-repository'; +import type { ITelegramService } from '../../domain/ports/telegram-service'; +import type { AppConfig } from '../../config/index'; +import type { UploadInput, UploadOutput } from '../dto/upload'; + +export interface UploadFileUseCaseDeps { + fileRepo: IFileRepository; + filePartRepo: IFilePartRepository; + telegramService: ITelegramService; + config: AppConfig; +} + +export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) { + return async (input: UploadInput): Promise => { + // 1. Check dedup (fileRepo.findByHash) + // 2. Check file size limits + // 3. Determine storage strategy: chunked vs single vs batch + // 4. Store via telegramService + // 5. Insert DB record via fileRepo + // 6. Build and return UploadOutput + }; +} +``` + +- [ ] **Create `src/application/use-cases/get-file.ts`** — Extract file redirect/stream logic from `src/routes/files.ts`. + +- [ ] **Create `src/application/use-cases/authenticate.ts`** — Extract login/logout/session logic from `src/routes/auth.ts` + `src/utils/auth.ts`. + +- [ ] **Compile check** + +```bash +bun build src/application/use-cases/upload-file.ts --target=bun --outfile=/dev/null 2>&1 | head -15 +``` +Expected: No errors (may have non-functional code until controllers wired up — skip if not compile-ready) + +- [ ] **Commit** + +```bash +git add src/application/use-cases/authenticate.ts src/application/use-cases/upload-file.ts src/application/use-cases/get-file.ts +git commit -m "feat: create application use cases (upload, get-file, auth)" +``` + +#### Task 13: Create application/use-cases (bucket, s3-object, multipart) + +**Files:** +- Create: `src/application/use-cases/manage-bucket.ts` +- Create: `src/application/use-cases/s3-object.ts` +- Create: `src/application/use-cases/multipart-upload.ts` + +**Interfaces:** +- Consumes: Repository interfaces from domain/ports, DTOs from application/dto +- Produces: Factory functions for bucket CRUD, S3 object ops, multipart upload + +- [ ] **Create `src/application/use-cases/manage-bucket.ts`** — Extract bucket CRUD logic from `src/routes/web-api.ts` + `src/routes/s3.ts`. + +- [ ] **Create `src/application/use-cases/s3-object.ts`** — Extract S3 get/put/delete/copy logic from `src/routes/s3.ts`. This is the biggest extraction. + +- [ ] **Create `src/application/use-cases/multipart-upload.ts`** — Extract S3 multipart upload logic from `src/routes/s3.ts`. + +- [ ] **Commit** + +```bash +git add src/application/use-cases/manage-bucket.ts src/application/use-cases/s3-object.ts src/application/use-cases/multipart-upload.ts +git commit -m "feat: create application use cases (bucket, s3-object, multipart)" +``` + +--- + +### Phase 5: Interfaces Layer — Controllers, Middleware, Routes, S3, Bot + +#### Task 14: Create interfaces/s3 protocol files + +**Files:** +- Create: `src/interfaces/s3/auth.ts` +- Create: `src/interfaces/s3/headers.ts` +- Create: `src/interfaces/s3/object-stream.ts` +- Create: `src/interfaces/s3/range.ts` +- Create: `src/interfaces/s3/virtual-host.ts` +- Create: `src/interfaces/s3/xml.ts` + +**Interfaces:** +- Produces: SigV4 auth, S3 headers, object streaming, range parsing, virtual-host extraction, XML builders — all same as `src/utils/s3/*` + +- [ ] **Create each file** — Move content from `src/utils/s3/auth.ts`, `src/utils/s3/headers.ts`, etc. Add JSDoc to each exported function. Same implementations. + +- [ ] **Compile check** + +```bash +bun build src/interfaces/s3/auth.ts --target=bun --outfile=/dev/null 2>&1 | head -5 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/interfaces/s3/ +git commit -m "feat: create interfaces/s3 protocol layer" +``` + +#### Task 15: Create HTTP middleware (auth, rate-limit) + +**Files:** +- Create: `src/interfaces/http/middleware/auth.ts` +- Create: `src/interfaces/http/middleware/rate-limit.ts` + +**Interfaces:** +- Produces: `requireAuth`, `getAuthSession`, `withRateLimit`, `checkRateLimit`, `cleanupRateLimitCache` + +- [ ] **Create `src/interfaces/http/middleware/auth.ts`** — Move auth middleware from `src/utils/auth.ts`. Update imports to use `config` from `../../config/index`. Add JSDoc. + +- [ ] **Create `src/interfaces/http/middleware/rate-limit.ts`** — Move rate limiter from `src/utils/rateLimit.ts`. Add JSDoc. + +- [ ] **Compile check** + +```bash +bun build src/interfaces/http/middleware/auth.ts --target=bun --outfile=/dev/null 2>&1 | head -10 +``` +Expected: No errors + +- [ ] **Commit** + +```bash +git add src/interfaces/http/middleware/ +git commit -m "feat: create HTTP middleware layer" +``` + +#### Task 16: Create HTTP controllers + +**Files:** +- Create: `src/interfaces/http/controllers/upload-controller.ts` +- Create: `src/interfaces/http/controllers/file-controller.ts` +- Create: `src/interfaces/http/controllers/auth-controller.ts` +- Create: `src/interfaces/http/controllers/home-controller.ts` +- Create: `src/interfaces/http/controllers/health-controller.ts` +- Create: `src/interfaces/http/controllers/s3-controller.ts` +- Create: `src/interfaces/http/controllers/web-api-controller.ts` + +**Interfaces:** +- Consumes: Use case factory functions from application layer +- Produces: HTTP handler functions compatible with `Bun.serve()` routes + +- [ ] **Create `src/interfaces/http/controllers/upload-controller.ts`** — Extract upload HTTP handling from `src/routes/upload.ts`. Controller parses request, calls use case, formats response: + +```typescript +import { createUploadFileUseCase } from '../../../application/use-cases/upload-file'; +// ... setup use case with dependencies +// ... handler functions for multipart and JSON upload paths +``` + +- [ ] **Create `src/interfaces/http/controllers/file-controller.ts`** — Extract from `src/routes/files.ts`. Controller handles params, calls getFile use case, handles redirect/stream. + +- [ ] **Create `src/interfaces/http/controllers/auth-controller.ts`** — Extract from `src/routes/auth.ts`. + +- [ ] **Create `src/interfaces/http/controllers/home-controller.ts`** — Move from `src/routes/home.ts`. + +- [ ] **Create `src/interfaces/http/controllers/health-controller.ts`** — Move from `src/routes/health.ts`. + +- [ ] **Create `src/interfaces/http/controllers/s3-controller.ts`** — Extract S3 dispatching from `src/routes/s3.ts`. This is the largest extraction — split into clear sections (bucket ops, object ops, multipart ops). The 1179-line file becomes a focused controller that delegates to use cases. + +- [ ] **Create `src/interfaces/http/controllers/web-api-controller.ts`** — Extract from `src/routes/web-api.ts`. + +- [ ] **Move `src/home.html` to `src/interfaces/http/controllers/home.html`** — Update reference in home-controller. + +- [ ] **Commit** + +```bash +git add src/interfaces/http/controllers/ +git commit -m "feat: create HTTP controller layer" +``` + +#### Task 17: Create HTTP route definitions + +**Files:** +- Create: `src/interfaces/http/routes/index.ts` + +**Interfaces:** +- Consumes: All controller handler functions +- Produces: Route table for `Bun.serve()` + +- [ ] **Create `src/interfaces/http/routes/index.ts`** — Define route table matching current `src/index.ts` routes. Each route maps to its controller handler. Add JSDoc. + +```typescript +import type { Server } from 'bun'; +import { handleUpload } from '../controllers/upload-controller'; +import { handleFileRedirect, handleFileInfo } from '../controllers/file-controller'; +// ... other imports + +/** Defines all HTTP routes for the application */ +export const routes: Server['routes'] = { + '/api/upload': { POST: handleUpload }, + '/f/:public_id': { GET: handleFileRedirect }, + '/file/:public_id/info': { GET: handleFileInfo }, + '/health': { GET: handleHealth }, + '/docs': { GET: handleSwaggerHtml }, + '/swagger.json': { GET: handleSwaggerJson }, + // ... etc +}; +``` + +- [ ] **Commit** + +```bash +git add src/interfaces/http/routes/ +git commit -m "feat: create HTTP route definitions" +``` + +#### Task 18: Create interfaces/bot + +**Files:** +- Create: `src/interfaces/bot/handler.ts` + +**Interfaces:** +- Consumes: `ITelegramService`, use cases +- Produces: `startBot()` function for Telegram bot lifecycle + +- [ ] **Create `src/interfaces/bot/handler.ts`** — Move bot handler logic from `src/bot.ts`. Add JSDoc. Update imports to use new structure. + +- [ ] **Commit** + +```bash +git add src/interfaces/bot/ +git commit -m "feat: create bot interface layer" +``` + +--- + +### Phase 6: Entry Point Rewire + +#### Task 19: Rewrite src/index.ts + +**Files:** +- Modify: `src/index.ts` +- Modify (delete): `src/bot.ts` + +- [ ] **Rewrite `src/index.ts`** — Update to import from new structure. Server bootstrap, S3 detection, route table from `interfaces/http/routes/index`, lifecycle management. + +```typescript +import { serve } from 'bun'; +import { config } from './config/index'; +import { startBot } from './interfaces/bot/handler'; +import { routes } from './interfaces/http/routes/index'; +import { isS3Request } from './interfaces/s3/auth'; +import { handleS3Request } from './interfaces/http/controllers/s3-controller'; +import { extractS3BucketFromHost } from './interfaces/s3/virtual-host'; +import { fileInfoCache } from './infrastructure/cache/index'; +import { cleanupRateLimitCache } from './interfaces/http/middleware/rate-limit'; +import { logger } from './shared/logger/index'; +import { metricsCollector } from './shared/metrics/index'; +``` + +- [ ] **Remove `src/bot.ts`** (replaced by `src/interfaces/bot/handler.ts`) + +- [ ] **Run full test suite** + +```bash +bun test +``` +Expected: All tests PASS (old src files still exist for backward compat) + +- [ ] **Commit** + +```bash +git add src/index.ts src/interfaces/bot/handler.ts +git rm src/bot.ts +git commit -m "feat: rewire entry point to new architecture" +``` + +--- + +### Phase 7: Test Restructure + +#### Task 20: Create mirror test structure + +**Files:** +- Create: `test/unit/domain/entities/` (entity type tests) +- Create: `test/unit/application/use-cases/` (use case tests) +- Create: `test/unit/shared/` (util tests) +- Create: `test/integration/interfaces/http/` (route tests) +- Create: `test/integration/infrastructure/` (repo tests) + +- [ ] **Create test folder structure mirroring src/** + +```bash +mkdir -p test/unit/domain/entities \ + test/unit/application/use-cases \ + test/unit/shared \ + test/integration/interfaces/http \ + test/integration/infrastructure/persistence \ + test/integration/infrastructure/telegram +``` + +- [ ] **Move util tests** — Copy `test/env.test.ts` → `test/unit/config/env.test.ts`, update import path to `src/config/index`. Copy `test/file.test.ts` → `test/unit/shared/file.test.ts`, update imports. Copy `test/zip.test.ts` → `test/unit/shared/zip.test.ts`. + +- [ ] **Move route tests** — Copy `test/upload.test.ts` → `test/integration/interfaces/http/upload.test.ts`, update imports. Copy `test/files.test.ts` → `test/integration/interfaces/http/files.test.ts`. Copy `test/auth-routes.test.ts`, `test/health.test.ts`, `test/swagger.test.ts`. + +- [ ] **Move S3 tests** — Copy `test/s3-auth.test.ts` → `test/unit/interfaces/s3/auth.test.ts`. Copy `test/s3-range.test.ts`, `test/s3-operations.test.ts`, `test/s3-bucket-config.test.ts`, `test/s3-object-stream.test.ts`. + +- [ ] **Move infrastructure tests** — Copy `test/db.test.ts` → `test/unit/infrastructure/persistence/drizzle/schema.test.ts`. Copy `test/chunked-storage.test.ts` → `test/unit/infrastructure/telegram/chunked-storage.test.ts`. Copy `test/telegram.test.ts`, `test/telegramQueue.test.ts`. + +- [ ] **Keep original test files** — Do NOT delete them yet. Original tests still reference old source paths and need to keep passing for now. + +- [ ] **Update test runner scripts in package.json** — Add test patterns for new test locations: + +```json +"test:new": "bun test test/unit/ test/integration/", +"test:all": "bun test && bun test test/unit/ test/integration/" +``` + +- [ ] **Run both old and new tests** + +```bash +bun test test/env.test.ts +bun test test/unit/config/env.test.ts +``` +Expected: Both PASS + +- [ ] **Commit** + +```bash +git add test/unit/ test/integration/ package.json +git commit -m "test: restructure tests to mirror new architecture" +``` + +--- + +### Phase 8: Cleanup — Delete Old Files + +#### Task 21: Remove old source directories + +**Files:** Delete old source directories once new structure is verified. + +- [ ] **Delete old directories** (only after all tests pass with new imports) + +```bash +rm -rf src/db src/routes src/utils src/env.ts src/bot.ts +``` + +- [ ] **Run full test suite to confirm nothing is broken** + +```bash +bun test +bun test test/unit/ test/integration/ +``` +Expected: All PASS + +- [ ] **Commit** + +```bash +git rm -r src/db src/routes src/utils src/env.ts src/bot.ts +git commit -m "chore: remove legacy source directories after migration" +``` + +#### Task 22: Final verification + +- [ ] **Full test suite** + +```bash +bun run test:all +``` +Expected: All PASS + +- [ ] **Build verification** + +```bash +bun run build +``` +Expected: dist/index.js and dist/migrate.js compile without errors + +- [ ] **Lint check** + +```bash +bunx biome check src test/unit test/integration +``` +Expected: No errors + +- [ ] **Final commit** if any fixes were needed + +```bash +git add -A +git commit -m "chore: final cleanup after architecture migration" +``` + +--- + +## Spec Coverage Check + +| Spec Requirement | Covered In | +|-----------------|------------| +| New folder structure | Task 5-19 (all phases) | +| DDD layers | Phase 2 (domain), Phase 4 (application), Phase 3 (infrastructure), Phase 5 (interfaces) | +| Repository interfaces | Task 6 | +| Repository implementations | Task 8 | +| Controller/use-case separation | Task 12-13 (use cases), Task 16 (controllers) | +| Dependency injection (factory functions) | Task 12 | +| Telegram service interface | Task 6 (ITelegramService port) | +| Bot moved to interfaces/bot | Task 18 | +| S3 protocol moved to interfaces/s3 | Task 14 | +| Config moved to config/ | Task 1 | +| Shared utilities moved to shared/ | Task 2-4 | +| Error classes | Task 2 | +| JSDoc on all exports | All creation tasks | +| Test restructure | Task 20 | +| Zero behavior changes | No logic altered — only moved/extracted | +| Delete old files | Task 21 | +| Final verification | Task 22 |