Files
TeleUploader/docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md
T

951 lines
32 KiB
Markdown
Raw Normal View History

# 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/<file>` 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<File, 'id' | 'createdAt' | 'updatedAt'>;
```
- [ ] **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<File | null>;
findByPublicId(publicId: string): Promise<File | null>;
findByUniqueId(telegramFileUniqueId: string): Promise<File | null>;
findByBucketAndKey(bucketId: string, s3Key: string): Promise<File | null>;
create(file: NewFile): Promise<File>;
listByPrefix(
bucketId: string,
prefix: string,
delimiter: string | null,
maxKeys: number,
startAfter: string | null,
): Promise<{ objects: S3FileRecord[]; prefixes: string[] }>;
softDelete(bucketId: string, s3Key: string): Promise<boolean>;
softDeleteBatch(bucketId: string, keys: string[]): Promise<number>;
countByBucket(bucketId: string): Promise<number>;
findOrphansByBucket(bucketId: string): Promise<File[]>;
}
```
- [ ] **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<ForwardResult>;
getFileInfo(telegramFileId: string): Promise<TelegramFileInfo>;
enqueueUpload<T>(task: () => Promise<T>): Promise<T>;
}
```
- [ ] **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<File | null> {
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<T>`, `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<UploadOutput> => {
// 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 |