diff --git a/.agents/rules/memory-bank/architecture.md b/.agents/rules/memory-bank/architecture.md index dbe74d4..ad2051f 100644 --- a/.agents/rules/memory-bank/architecture.md +++ b/.agents/rules/memory-bank/architecture.md @@ -1,175 +1,250 @@ # System Architecture -## Architectural Style - -Clean Architecture with Domain-Driven Design (DDD) principles. - -## Layer Structure - -### 1. Domain Layer (`src/domain/`) - -**Purpose:** Core business logic and entities, independent of infrastructure. - -**Components per Domain:** - -- `entity/` - Business entities with domain logic -- `interface.ts` - Domain interfaces (Repository, Service, Handler) -- `repository/` - Repository implementations -- `usecase/` - Business logic and orchestration -- `handler/` - HTTP request handlers -- `request/` - DTOs for incoming requests -- `response/` - DTOs for outgoing responses -- `mock/` - Mock implementations for testing - -**Current Domains:** - -- `user/` - User authentication and profile management -- `task/` - Task management (example domain) - -### 2. Infrastructure Layer (`src/infrastructure/`) - -**Purpose:** External concerns and technical implementations. - -**Sub-packages:** - -- `config/` - Configuration management with dotenv/config -- `db/` - Database abstraction and factory -- `http/client/` - HTTP client wrapper -- `logger/` - Structured logging (pino wrapper) -- `password/` - Password hashing (Argon2id) -- `drizzle/` - Drizzle ORM database code - -### 3. Application Layer (`src/app/`) +**Last Updated:** 2026-02-10 + +## Architecture Patterns + +### Clean Architecture (Hexagonal) + +The project follows Clean Architecture principles with clear layer separation: + +```mermaid +graph TB + subgraph Presentation Layer + Handler[HTTP Handlers] + DTO[Data Transfer Objects] + end + + subgraph Business Logic Layer + Usecase[Use Cases] + Domain[Domain Entities] + end + + subgraph Infrastructure Layer + Repository[Repositories] + DB[Database] + Config[Configuration] + Logger[Logging] + end + + Handler --> Usecase + Usecase --> Domain + Usecase --> Repository + Repository --> DB + Handler --> DTO + Usecase --> Logger + Repository --> Config + + style Domain fill:#f9f,stroke:#333,stroke-width:2px +``` -**Purpose:** Application orchestration and dependency injection. +### Dependency Rule + +Dependencies point inward: +- Presentation depends on Business Logic +- Business Logic depends on nothing (pure domain) +- Infrastructure implements interfaces defined in Business Logic + +## Component Diagram + +```mermaid +graph LR + subgraph API Layer + Hono[Hono.js] + Middleware[Middleware] + Routes[Routes] + end + + subgraph Feature Layer + UserHandler[User Handler] + AuthMiddleware[Auth Middleware] + UserUsecase[User UseCase] + JWTUsecase[JWT UseCase] + UserDomain[User Domain] + JWTDomain[JWT Domain] + end + + subgraph Data Layer + UserRepo[User Repository] + DrizzleRepo[Drizzle ORM Repo] + MemoryRepo[Memory Repo] + end + + subgraph Infrastructure + Container[DI Container] + Config[Config] + DB[Better-SQLite3/Drizzle] + Logger[Logger] + end + + Hono --> Middleware + Hono --> Routes + Routes --> UserHandler + Routes --> AuthMiddleware + UserHandler --> UserUsecase + UserHandler --> JWTUsecase + AuthMiddleware --> JWTUsecase + UserUsecase --> UserDomain + UserUsecase --> UserRepo + JWTUsecase --> JWTDomain + UserRepo --> DrizzleRepo + UserRepo --> MemoryRepo + DrizzleRepo --> DB + Container --> Config + Container --> DB + Container --> Logger +``` -**Key Components:** +## Layer Responsibilities -- `app.ts` - Main application structure -- Dependency wiring -- Middleware setup +### Presentation Layer (`src/presentation/`) +- HTTP request/response handling with Hono.js +- Request validation using Zod +- Response formatting with standard structure - Route registration -- Server lifecycle management - -### 4. Entry Point (`src/`) - -**Purpose:** Application bootstrap. - -**Components:** - -- `index.ts` - Application initialization and startup - -## Component Boundaries - -### Domain Boundaries - -- Each domain is self-contained with its own interfaces -- Domains communicate through well-defined interfaces -- No direct dependencies between domains -- Shared infrastructure through interfaces only - -### Infrastructure Boundaries - -- Infrastructure implements domain interfaces -- Domain layer depends on abstractions, not implementations -- Database access abstracted through repository pattern -- External services abstracted through client interfaces - -## Data Flow - -### Request Flow - -``` -Client → Handler → UseCase → Repository → Database - ↓ ↓ ↓ - Request Business Data Access - DTO Logic Layer - ↓ ↓ ↓ - Response Entity Drizzle Query - DTO Mapping ORM +- Error translation to HTTP status codes + +### Business Logic Layer (`src/domain/`) +- Orchestration of business operations +- Domain entity coordination +- Business rule enforcement +- Error handling and logging +- Transaction coordination (when needed) + +### Domain Layer (`src/core/`) +- Core business entities (User, JWT claims) +- Domain-specific validation rules +- Business logic encapsulation +- No external dependencies +- Pure TypeScript code + +### Infrastructure Layer (`src/infrastructure/`) +- Data access implementations (Drizzle ORM, Better-SQLite3) +- External service integrations +- Configuration management +- Logging infrastructure +- Database connection management + +## Data Flow: User Authentication + +```mermaid +sequenceDiagram + participant Client + participant Handler + participant UserUsecase + participant JWTUsecase + participant Repository + participant Domain + + Client->>Handler: POST /auth/login + Handler->>Handler: Validate request (Zod) + Handler->>UserUsecase: Authenticate(email, password) + UserUsecase->>Repository: GetByEmail(email) + Repository->>Repository: Query database + Repository-->>UserUsecase: User entity + UserUsecase->>Domain: VerifyPassword(password) + Domain-->>UserUsecase: Valid/Invalid + UserUsecase-->>Handler: User entity + Handler->>JWTUsecase: GenerateTokenPair(user) + JWTUsecase->>JWTUsecase: Create access token + JWTUsecase->>JWTUsecase: Create refresh token + JWTUsecase-->>Handler: TokenPair + Handler-->>Client: Response with token ``` -### Authentication Flow - -1. User submits credentials to `/api/v1/auth/login` -2. Handler validates request DTO -3. UseCase retrieves user by email -4. Password verified using Argon2id -5. JWT token generated with user ID and email -6. Token returned in response - -### Protected Route Flow - -1. Request includes JWT in Authorization header -2. JWT middleware validates token -3. User ID extracted from token context -4. Handler processes request with user context -5. UseCase enforces ownership rules - -## Module Interactions - -### Dependency Injection - -- Application layer creates all dependencies -- Dependencies passed through constructors -- Interfaces used for all external dependencies -- Mock implementations for testing - -### Database Access Patterns - -- Drizzle ORM provides type-safe queries -- Repository pattern abstracts database -- Transactions managed at repository level -- Connection pooling handled by pg (node-postgres) - -### Error Handling Strategy - -- Domain-specific errors in usecase layer -- Repository errors wrapped with context -- HTTP status codes mapped in handler layer -- Structured error responses to clients +## Data Flow: User Creation + +```mermaid +sequenceDiagram + participant Client + participant Handler + participant UserUsecase + participant Repository + participant Domain + + Client->>Handler: POST /users + Handler->>Handler: Validate request (Zod) + Handler->>UserUsecase: CreateUser(request) + UserUsecase->>Repository: Exists(email) + Repository-->>UserUsecase: false + UserUsecase->>Domain: NewUser(email, name, password) + Domain->>Domain: Validate email + Domain->>Domain: Hash password (Argon2id/Bcrypt) + Domain-->>UserUsecase: User entity + UserUsecase->>Repository: Create(user) + Repository-->>UserUsecase: nil + UserUsecase-->>Handler: User entity + Handler-->>Client: 201 Created with user data +``` ## Integration Points -### External Services - -- PostgreSQL database (primary data store) -- Future: Redis (caching) -- Future: Message queues (async processing) +### External Integrations +- **Better-SQLite3:** Primary data store via Drizzle ORM +- **Configuration:** Dotenv for environment variables +- **Logging:** Bun's built-in logger or Pino.js -### API Integration +### Internal Integrations +- **Dependency Injection:** Container manages all dependencies +- **Middleware:** Hono middleware for cross-cutting concerns +- **Drizzle ORM:** Type-safe SQL query generation +- **Swagger/OpenAPI:** Auto-generated API documentation with Scalar -- RESTful API endpoints -- OpenAPI documentation at `/swagger/*` or `/docs` -- Health checks at `/health` and `/readiness` +## Feature Structure -## Scalability Considerations +Each feature follows this structure: -### Horizontal Scaling - -- Stateless application design -- JWT tokens (no session storage) -- Database connection pooling -- Rate limiting per client - -### Vertical Scaling +``` +feature// +├── core/ # Business entities and rules +├── dto/ # Data transfer objects +├── presentation/ # HTTP handlers +├── usecase/ # Business logic +└── repository/ # Data access interfaces +``` -- Configurable database connections -- Efficient query generation -- Connection pooling optimization -- Memory-efficient data structures +### Current Features +1. **auth** - JWT authentication and middleware +2. **user** - User CRUD operations + +### Adding New Features +Follow the established pattern: +1. Define domain entities in `core/` +2. Create repository interface in `repository/` +3. Implement usecases in `usecase/` +4. Create handlers in `presentation/` +5. Define DTOs in `dto/` +6. Register routes in `src/routes.ts` + +## Dependency Injection + +The container uses a service locator pattern with Bun's DI container: + +```typescript +const container = new Container(); +container.register('config', () => new Config()); +container.register('db', () => new Database(container.get('config'))); +container.register('userRepository', () => + config.database.enabled + ? new DrizzleUserRepository(container.get('db')) + : new MemoryUserRepository() +); +``` -## Deployment Architecture +Auto-selection based on database availability: +- If DB configured → Drizzle repository +- If DB not configured → In-memory repository -### Container Strategy +## Error Handling Strategy -- Single container for application -- Docker Compose for local development -- Environment-specific configurations -- Health checks for orchestration +- **Domain Errors:** Defined in core layer (e.g., `InvalidEmailError`) +- **App Errors:** Wrapped in `src/core/errors/` with HTTP mapping +- **Logging:** All errors logged with context +- **Response:** Standardized error format with code/message/details -### Configuration Management +## Bun Runtime Optimizations -- Environment variable configuration -- dotenv/config for configuration loading -- Type-safe configuration with Zod schemas +- **Hot Reloading:** Bun's built-in watch mode during development +- **TypeScript:** Native support without transpilation +- **Benchmarks:** Bun's native test runner for fast tests +- **Bundle:** Bun's bundler for production builds diff --git a/.agents/rules/memory-bank/brief.md b/.agents/rules/memory-bank/brief.md index 5cf0017..b483995 100644 --- a/.agents/rules/memory-bank/brief.md +++ b/.agents/rules/memory-bank/brief.md @@ -1,21 +1,81 @@ -# Zercle Bun Template - Project Brief +# Project Brief -**Project Name:** zercle-bun-template +**Last Updated:** 2026-02-10 -**Purpose:** Production-ready RESTful API template built with Bun runtime and Hono framework, featuring clean architecture, JWT authentication, and PostgreSQL database. +## Project Identity -**Target Audience:** Developers looking for a solid foundation to build Bun microservices or REST APIs with best practices already implemented. +- **Name:** zercle-hono-template +- **Type:** Production-ready Hono.js web application template with Bun runtime +- **Runtime:** Bun 1.x +- **Language:** TypeScript 5.x -**Key Value Proposition:** +## Purpose -- Clean architecture with domain-driven design principles -- Type-safe database operations using Drizzle ORM -- Comprehensive testing infrastructure (unit, integration, mocks) -- Production-ready features (JWT auth, rate limiting, CORS, logging) -- Docker support for easy deployment -- OpenAPI documentation out of the box -- Fast runtime with Bun's performance +This project provides a comprehensive, production-ready Hono.js web application template following Clean Architecture and Hexagonal principles. It serves as a foundation for building scalable, maintainable web services with JWT authentication, user management, and a well-structured codebase, leveraging Bun's high-performance runtime. -**Current Status:** Active development template with User and Task domains as working examples. +## Core Requirements -**Repository:** github.com/zercle/zercle-bun-template +1. **Clean Architecture Implementation** + - Feature-based organization + - Clear separation of concerns (Presentation → Business Logic → Infrastructure) + - Dependency injection for loose coupling + +2. **Authentication & Security** + - JWT-based authentication with access/refresh tokens + - Bcrypt/Argon2 password hashing + - Secure configuration management + +3. **Data Persistence** + - Better-SQLite3 database with Drizzle ORM for type-safe queries + - In-memory repository for testing/development + - Repository pattern for data access abstraction + +4. **API Design** + - RESTful API with Hono.js framework + - OpenAPI/Swagger documentation via Scalar + - Standardized response format + +5. **Developer Experience** + - Comprehensive testing (unit, integration) + - Type-safe development with TypeScript + - Structured logging with Bun's built-in logger or Pino + - Environment-based configuration + +## Target Audience + +- Backend developers building Hono.js web services +- Teams requiring a production-ready starting point +- Projects needing Clean Architecture implementation +- Applications requiring JWT authentication +- Services requiring SQLite integration with Drizzle ORM + +## Key Features + +- User CRUD operations with validation +- Email/password authentication +- JWT token generation and validation +- Password hashing with Bcrypt/Argon2 +- Configurable repository implementations +- Graceful shutdown handling +- Request/response logging middleware +- Health check endpoints +- OpenAPI documentation with Scalar + +## Project State + +The template is production-ready with: +- Complete implementation of user and auth features +- Dual repository implementations (memory + SQLite/Drizzle) +- Comprehensive test coverage +- Docker support for containerization +- CI/CD configuration (pre-commit hooks, ESLint, Prettier) +- Migration scripts for database schema + +## Bun Runtime Benefits + +- **Fast Startup:** Near-instant cold starts +- **Native TypeScript:** No transpilation step required +- **Hot Reloading:** Instant reload during development +- **High Performance:** 3-4x faster than Node.js for many workloads +- **Native Testing:** Built-in test runner with Jest-compatible API +- **Bundle Management:** Fast npm/yarn/pnpm alternative with Bun install diff --git a/.agents/rules/memory-bank/context.md b/.agents/rules/memory-bank/context.md index 13344f3..a87d876 100644 --- a/.agents/rules/memory-bank/context.md +++ b/.agents/rules/memory-bank/context.md @@ -1,456 +1,262 @@ -# Context & Decisions - -## Architectural Decisions - -### Clean Architecture Choice - -**Decision:** Adopted Clean Architecture with DDD principles -**Rationale:** Separates business logic from infrastructure, improves testability, enables independent evolution of layers -**Impact:** All new domains must follow the established layer structure - -### Drizzle ORM for Database Access - -**Decision:** Use Drizzle ORM instead of raw SQL or other ORMs -**Rationale:** Type-safe queries, excellent TypeScript support, better performance than heavy ORMs, explicit SQL control when needed -**Impact:** All database queries use Drizzle ORM, schema definitions in `src/drizzle/schema/` - -### JWT Stateless Authentication - -**Decision:** JWT tokens without server-side session storage -**Rationale:** Stateless design enables horizontal scaling, simpler architecture, no session management overhead -**Impact:** All protected routes require JWT middleware, tokens stored client-side - -### Argon2id for Password Hashing - -**Decision:** Argon2id algorithm for password hashing -**Rationale:** Memory-hard, resistant to GPU/ASIC attacks, recommended by security experts -**Impact:** All password operations must use the password.Hasher wrapper - -### Hono Framework - -**Decision:** Hono as HTTP framework -**Rationale:** High performance, minimal boilerplate, excellent middleware support, Edge runtime compatible, active TypeScript community -**Impact:** All HTTP handlers use Hono context and patterns - -## Domain Rules - -### User Domain - -**Business Rules:** - -- Email must be unique across all users -- Password must be hashed before storage -- Users can only update their own profiles -- Email cannot be changed after registration -- Minimum full name length: 2 characters - -**Validation Rules:** - -- Email format validated by Zod schemas -- Password strength enforced by Argon2id parameters -- Phone number is optional -- Full name required for registration - -**Ownership Rules:** - -- Users can only access their own profile -- Admin endpoints (if added) can access all users -- User ID extracted from JWT token for authorization - -### Task Domain - -**Business Rules:** - -- Tasks must have an owner (user_id) -- Users can only access their own tasks -- Task status must be one of: pending, in_progress, completed, cancelled -- Task priority must be one of: low, medium, high, urgent -- Completed tasks automatically set completed_at timestamp - -**Validation Rules:** - -- Title is required -- Description is optional -- Due date is optional -- Status defaults to "pending" -- Priority defaults to "medium" if not specified - -**Ownership Rules:** - -- All task operations verify user ownership -- Cannot access/modify tasks owned by other users -- Task list filtered by user_id - -## File & Component Summaries - -### Core Application Files - -**src/index.ts** - -- Application entry point -- Loads environment-specific configuration -- Initializes logger and application -- Handles graceful shutdown - -**src/app/app.ts** - -- Main application structure -- Dependency injection container -- Middleware setup (RequestID, Logger, Recovery, CORS, RateLimit) -- Route registration -- Server lifecycle management - -### Configuration - -**src/infrastructure/config/config.ts** - -- Configuration structs for all components -- dotenv/config-based configuration loading -- Environment variable support -- Type-safe configuration access with Zod schemas - -**.env.example** - -- Environment-specific configurations -- local, dev, uat, prod environments -- Database, JWT, logging, CORS, rate limit settings - -### Database Layer - -**src/infrastructure/db/postgres.ts** - -- PostgreSQL database implementation -- Connection pooling configuration -- Health check implementation -- Drizzle ORM integration - -**src/infrastructure/db/factory.ts** - -- Database factory for creating connections -- Abstracts database type selection -- Currently supports PostgreSQL only - -**src/drizzle/schema/** - -- Drizzle schema definitions -- Type-safe database models -- Table definitions and relationships - -**src/drizzle/migrations/** - -- Database migration files -- Up and down migrations -- Versioned schema changes - -### Domain: User - -**src/domain/user/entity/user.ts** - -- User entity definition -- UUID-based primary key -- Fields: id, email, password, full_name, phone, timestamps - -**src/domain/user/repository/repository.ts** - -- Drizzle-based repository implementation -- CRUD operations for users -- Email uniqueness check -- Pagination support - -**src/domain/user/usecase/usecase.ts** - -- Business logic for user operations -- Register, Login, GetProfile, UpdateProfile, DeleteAccount, ListUsers -- Password hashing and verification -- JWT token generation -- Domain-specific error definitions - -**src/domain/user/handler/handler.ts** - -- HTTP handlers for user endpoints -- Request/response DTO mapping -- Error handling and HTTP status codes -- Route registration - -### Domain: Task - -**src/domain/task/entity/task.ts** - -- Task entity definition -- UUID-based primary key -- Fields: id, user_id, title, description, status, priority, due_date, completed_at, timestamps - -**src/domain/task/repository/repository.ts** - -- Drizzle-based repository implementation -- CRUD operations for tasks -- User filtering for list operations -- Ownership verification - -**src/domain/task/usecase/usecase.ts** - -- Business logic for task operations -- CreateTask, GetTask, ListTasks, UpdateTask, DeleteTask -- Status and priority validation -- Ownership enforcement -- Domain-specific error definitions - -**src/domain/task/handler/handler.ts** - -- HTTP handlers for task endpoints -- Request/response DTO mapping -- Error handling and HTTP status codes -- Protected routes only - -### Infrastructure Components - -**src/infrastructure/logger/logger.ts** - -- Pino-based structured logger -- Configurable log levels and format -- Request ID integration -- Context-aware logging - -**src/infrastructure/password/passworder.ts** - -- Argon2id password hashing wrapper -- Configurable parameters -- Hash and verify operations - -**src/infrastructure/http/client/httpClient.ts** - -- HTTP client wrapper (using fetch) -- For making external HTTP requests -- Configurable timeouts and retries - -**src/middleware/** - -- Custom middleware implementations -- JWT authentication -- Request ID generation -- Structured logging -- CORS handling -- Rate limiting - -**src/health/** - -- Health check handler -- Database connectivity check -- Readiness probe - -## Dependency Mapping - -### Domain Dependencies - -- **User Domain:** Depends on config, logger, password, middleware (JWT) -- **Task Domain:** Depends on logger only (uses Drizzle directly for DB) - -### Infrastructure Dependencies - -- **Database:** pg (node-postgres) driver -- **Config:** dotenv/config -- **Logging:** pino -- **Validation:** zod -- **Auth:** jose or similar JWT library -- **Password:** argon2 or similar - -### External Dependencies - -- **PostgreSQL:** Primary database -- **Testcontainers:** Integration testing (via bun test) -- **OpenAPI:** API documentation - -## Key Implementation Details - -### JWT Token Structure - -- Contains user ID and email in claims -- Configurable expiration time -- Secret key from configuration -- Bearer token format in Authorization header - -### Database Connection Pool - -- Min connections: 5 -- Max connections: 25 -- Connection lifetime: 1 hour -- Idle timeout: 10 minutes -- Health check period: 1 minute - -### Rate Limiting - -- Configurable requests per time window -- Default: 100 requests per 60 seconds -- Applied at middleware level -- Per-client tracking - -### CORS Configuration - -- Allowed origins configurable per environment -- Local: localhost:3000, localhost:8080 -- Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS -- Headers: Authorization, Content-Type, X-Request-ID - -### Error Handling Pattern - -```typescript -// UseCase layer: Domain errors -if (!user) { - return ErrUserNotFound; -} - -// Repository layer: Wrap with context -if (err) { - throw new Error(`Failed to create user: ${err.message}`); -} - -// Handler layer: Map to HTTP status -if (err instanceof ErrUserNotFound) { - return c.json({ error: "User not found" }, 404); -} +# Current Work Context + +**Last Updated:** 2026-02-10 + +## Migration from Go Echo to Hono.js/Bun + +### Rationale +- **Performance:** Bun's native performance advantages over Go/Node.js +- **TypeScript:** First-class TypeScript support without transpilation +- **Developer Experience:** Fast hot reloading, built-in testing, bundling +- **Simplicity:** Lighter dependency footprint than Go ecosystems +- **Ecosystem:** Growing Hono.js community for API development + +### Key Changes +- **Framework:** Echo v4 → Hono.js +- **Language:** Go → TypeScript +- **Database:** PostgreSQL/pgx v5 → Better-SQLite3/Drizzle ORM +- **Logging:** Zerolog → Bun's built-in logger or Pino.js +- **Configuration:** Viper → Dotenv +- **ORM:** sqlc → Drizzle ORM +- **JWT:** golang-jwt/jwt → jose/jsonwebtoken +- **Validation:** go-playground/validator → Zod +- **Testing:** testify → Bun test/Vitest +- **Build:** Go build → Bun bundle + +## Current State + +### Completed Features +1. **User Management** + - Create, read, update, delete users + - Email uniqueness validation + - Password update with verification + - Paginated listing + +2. **Authentication** + - JWT token generation (access + refresh) + - Token validation middleware + - Password hashing with Bcrypt + - Login endpoint + +3. **Infrastructure** + - Configuration management (dotenv) + - Structured logging + - Database connection management + - Dependency injection container + +4. **Testing** + - Unit tests for all components + - Integration tests for repositories + - Type checking with TypeScript + +### Codebase Structure ``` - -### Request Validation - -- Use Zod schemas for validation -- Validate before business logic -- Return validation errors with field details -- Example: `z.string().email()` - -### Pagination Pattern - -```typescript -// Standard pagination parameters -const { limit, offset } = getPaginationParams(c); - -// Repository returns data + total count -const { users, total } = await repo.list(ctx, limit, offset); - -// Response includes pagination metadata -return c.json({ - users, - total, - limit, - offset, -}); +zercle-hono-template/ +├── src/ +│ ├── core/ # Domain entities and business rules +│ │ ├── entities/ # User, JWT claims, etc. +│ │ ├── errors/ # Custom error types +│ │ └── types/ # TypeScript types +│ ├── domain/ # Business logic interfaces +│ │ ├── repositories/ # Repository interfaces +│ │ └── services/ # Service interfaces +│ ├── features/ +│ │ ├── auth/ # Authentication feature +│ │ │ ├── core/ # JWT domain logic +│ │ │ ├── middleware/ # Auth middleware +│ │ │ └── usecase/ # JWT business logic +│ │ └── user/ # User management feature +│ │ ├── core/ # User domain entities +│ │ ├── dto/ # Data transfer objects +│ │ ├── presentation/ # HTTP handlers +│ │ ├── usecase/ # Business logic +│ │ └── repository/ # Data access +│ ├── infrastructure/ +│ │ ├── db/ # Database layer +│ │ │ ├── migrations/ # Database migrations +│ │ │ ├── repositories/ # Drizzle implementations +│ │ │ └── schema/ # Drizzle schema +│ │ ├── di/ # Dependency injection +│ │ └── logging/ # Logging infrastructure +│ ├── presentation/ # HTTP layer +│ │ ├── handlers/ # Hono handlers +│ │ ├── middleware/ # Cross-cutting middleware +│ │ └── routes/ # Route definitions +│ └── app.ts # Application entry point +├── configs/ # Configuration files +├── .agents/rules/memory-bank/ # Memory bank +├── .github/workflows/ # CI/CD +└── .pre-commit-config.yaml # Pre-commit hooks ``` -## Testing Strategy - -### Unit Tests - -- Test usecase business logic -- Mock repository dependencies -- Test error paths and edge cases -- Located in same directory as implementation with `.test.ts` suffix - -### Integration Tests - -- Test API endpoints end-to-end -- Use testcontainers for real database -- Test authentication flow -- Located in test/integration/ - -### Mock Generation - -- Use vi (Vitest) for mocking -- Generate mocks from domain interfaces -- Located in domain/\*/mock/ directories -- Regenerate when interfaces change - -### Test Helpers - -- test/mock/dbMock.ts - Database mock utilities -- test/integration/testHelper.ts - Integration test setup -- Common test fixtures and utilities - -## Migration Strategy - -### Database Migrations - -- Drizzle Kit migration format -- Up and down migrations required -- Version naming: timestamp_description -- Apply migrations in order -- Rollback support with down migrations - -### Schema Changes - -- Add new migrations for schema changes -- Never modify existing migrations -- Use Drizzle to regenerate schema after changes -- Test migrations in all environments - -## Configuration Management - -### Environment Hierarchy - -1. Base config from .env file -2. Environment variable overrides -3. Default values in Zod schemas +## Known Patterns and Conventions -### Configuration Files - -- `.env.example` - Example configuration -- `.env.local` - Local development (not committed) -- `.env.dev` - Development environment -- `.env.uat` - User acceptance testing -- `.env.prod` - Production - -### Environment Variables - -- `NODE_ENV` - Environment selector (default: local) -- Database credentials via env vars in production -- JWT secret via env vars in production -- Never commit secrets to repository - -## Deployment Considerations - -### Docker Deployment - -- Multi-stage build for optimization -- Alpine-based final image -- Non-root user for security -- Health checks configured -- Port 3000 exposed - -### Database Requirements - -- PostgreSQL 12+ required -- Connection pool configuration important -- Migrations must be applied before startup -- Health check verifies connectivity - -### Monitoring Points - -- Health check endpoints -- Request/response logging -- Error logging with context -- Performance metrics (future) -- Database query performance (future) - -## Known Constraints - -### Current Limitations - -- Only PostgreSQL supported (no MySQL, SQLite) -- No caching layer implemented -- No message queue integration -- No distributed tracing -- No metrics collection -- Single-region deployment only +### Feature Organization +Each feature follows this structure: +``` +feature// +├── core/ # Pure domain logic, no dependencies +├── dto/ # Request/response structures +├── presentation/ # HTTP handlers (presentation) +├── usecase/ # Business logic (orchestration) +└── repository/ # Data access interfaces +``` -### Technical Debt +### Dependency Injection +- Use `Container` class from `infrastructure/di` +- Register dependencies with `container.register(key, factory)` +- Resolve with `container.get(key)` +- Auto-selection based on configuration + +### Error Handling +- Domain errors in `core/errors/` packages +- App errors in `src/core/errors/` +- HTTP status code mapping +- Structured error logging +- User-friendly error messages + +### Logging +- Use Bun's built-in logger or Pino.js +- Context-aware logging with `logger.child()` +- Field-based logging +- Log levels: debug, info, warn, error, fatal + +### Testing +- Bun's built-in test runner (`bun test`) +- Table-driven tests for multiple scenarios +- Integration tests with real database +- Test naming: `*.test.ts`, `*.integration.test.ts` +- Coverage with `bun test --coverage` + +### Repository Pattern +- Interface defined in `repository/` +- Implementations: `MemoryUserRepository`, `DrizzleUserRepository` +- Context support for all operations +- Error handling with typed errors -- Consider adding caching layer -- Consider standardizing database access patterns -- Evaluate alternative ORM options +### Configuration +- Environment variables via dotenv +- Environment prefix: `APP_` +- Type-safe configuration with Zod validation +- Validation on load -### Future Considerations +## Dependencies -- Add Redis caching layer -- Implement message queue for async operations -- Add Prometheus metrics -- Implement distributed tracing with OpenTelemetry -- Add GraphQL support as alternative to REST -- Consider gRPC for internal service communication +### External Dependencies +- **Hono.js:** Web framework +- **Drizzle ORM:** Type-safe SQL query builder +- **Better-SQLite3:** SQLite driver +- **jose:** JWT library +- **Zod:** Validation +- **Bcrypt:** Password hashing + +### Internal Dependencies +- `src/core` - Domain logic +- `src/infrastructure/di` - DI container +- `src/infrastructure/db` - Database layer +- `src/presentation` - HTTP handlers + +## Next Steps + +### Immediate Priorities +1. **Refresh Token Endpoint** + - Implement refresh token validation + - Generate new access tokens + - Update JWT usecase + +2. **Token Revocation** + - Implement token blacklist + - Add logout endpoint + - Handle token expiration gracefully + +3. **Rate Limiting** + - Add rate limiting middleware + - Configure per-IP and per-user limits + - Integrate with Redis (optional) + +### Medium-term Enhancements +1. **RBAC Implementation** + - Define role/permission models + - Add role-based middleware + - Implement permission checking + +2. **Email Verification** + - Add email verification flow + - Generate verification tokens + - Send verification emails + +3. **Password Reset** + - Implement reset token generation + - Add password reset endpoint + - Send reset emails + +### Long-term Considerations +1. **Caching Layer** + - Redis integration + - Cache frequently accessed data + - Implement cache invalidation + +2. **Event-Driven Architecture** + - Add event bus + - Implement event publishing + - Add event consumers + +3. **GraphQL Support** + - Add GraphQL server (Hono has GraphQL adapters) + - Define schema + - Implement resolvers + +## Known Issues + +None currently documented. + +## Decisions Log + +### 2026-02: Go Echo → Hono.js/Bun Migration +- **Decision:** Migrate from Go Echo to Hono.js with Bun runtime +- **Rationale:** Bun's performance, TypeScript native support, growing ecosystem +- **Impact:** Complete rewrite of codebase +- **Status:** In Progress + +### 2026-02: Drizzle ORM Integration +- **Decision:** Use Drizzle ORM for type-safe SQL queries +- **Rationale:** Type safety, migration support, lightweight footprint +- **Alternatives Considered:** Prisma, TypeORM, raw SQL +- **Status:** Completed + +### 2026-02: Better-SQLite3 Selection +- **Decision:** Use Better-SQLite3 with Drizzle ORM +- **Rationale:** Synchronous API (faster), native bindings, small footprint +- **Alternatives Considered:** libSQL, PostgreSQL with pgx +- **Status:** Completed + +### Initial: Bcrypt for Password Hashing +- **Decision:** Use Bcrypt for password hashing +- **Rationale:** Widely supported, good security properties +- **Alternatives Considered:** Argon2id, scrypt, PBKDF2 +- **Status:** Implemented with configurable cost factor + +## Code Quality Standards + +### Linting +- ESLint with TypeScript and Hono.js plugins +- Pre-commit hooks enforce standards +- CI/CD integration for PR validation + +### Testing +- 80%+ coverage for critical paths +- All business logic tested +- Integration tests for database operations +- Bun's built-in test runner + +### Documentation +- OpenAPI/Scalar for API docs +- Inline comments for complex logic +- README for project overview +- Memory bank for architectural decisions + +### TypeScript Strictness +- `strict: true` in tsconfig.json +- No `any` types allowed +- Explicit return types for functions +- Full null checking enabled diff --git a/.agents/rules/memory-bank/product.md b/.agents/rules/memory-bank/product.md index b5ad3f1..22c5ed2 100644 --- a/.agents/rules/memory-bank/product.md +++ b/.agents/rules/memory-bank/product.md @@ -1,99 +1,116 @@ -# Product Goals & Features - -## Product Vision - -To provide a production-ready, well-archityped Bun template that accelerates API development while maintaining code quality, security, and scalability. - -## Core Features - -### Authentication & Authorization - -- JWT-based authentication with configurable expiration -- Argon2id password hashing for secure storage -- User registration, login, and profile management -- Protected routes with JWT middleware +# Product Context + +**Last Updated:** 2026-02-10 + +## Problems Solved + +1. **Architecture Complexity** + - Provides a battle-tested Clean Architecture implementation + - Eliminates architectural decision paralysis for new projects + - Establishes clear layer boundaries and dependency rules + - Leverages TypeScript for type safety across all layers + +2. **Runtime Performance** + - Bun's native performance (3-4x faster than Node.js) + - Native TypeScript execution without transpilation overhead + - Fast cold starts for serverless deployments + - Efficient memory usage + +3. **Security Implementation** + - Industry-standard JWT authentication + - Bcrypt password hashing + - Secure configuration management with dotenv + - Input validation with Zod + +4. **Development Velocity** + - Fast hot reloading during development + - Built-in testing with coverage + - Type-safe development with TypeScript + - Comprehensive linting and formatting + +5. **Database Integration** + - Type-safe queries with Drizzle ORM + - Easy switching between in-memory and SQLite + - Migrations support with Drizzle Kit + - Small footprint with Better-SQLite3 + +## UX Goals + +- **Developer Experience:** Intuitive code structure with clear TypeScript conventions +- **Onboarding:** Comprehensive documentation and examples +- **Extensibility:** Easy to add new features following established patterns +- **Testing:** First-class testing support with Bun's test runner +- **Observability:** Structured logging for debugging and monitoring +- **Performance:** Fast startup times and low memory usage + +## Key Features + +### Authentication +- Email/password login with JWT tokens +- Access token (15 min default TTL) and refresh token (7 days default) +- Token validation middleware +- Optional authentication for public endpoints ### User Management - -- User registration with email validation -- Profile retrieval and updates -- User listing with pagination -- Account deletion - -### Task Management (Example Domain) - -- CRUD operations for tasks -- Task ownership verification -- Status tracking (pending, in_progress, completed, cancelled) -- Priority levels (low, medium, high, urgent) -- Due date management -- Pagination support - -### API Features - -- RESTful API design -- OpenAPI/Swagger documentation -- Request validation using Zod -- Structured error responses -- Health check endpoints - -## Non-Functional Requirements +- Create, read, update, delete users +- Email uniqueness validation +- Password update with old password verification +- Paginated user listing ### Security - -- Password hashing with Argon2id -- JWT token-based authentication -- CORS configuration -- Rate limiting (configurable requests per window) -- Input validation and sanitization +- Bcrypt password hashing with configurable cost factor +- Environment-based security defaults (production vs development) +- Constant-time password comparison +- Secure token generation with UUID jti + +### API Design +- RESTful endpoints following best practices +- Standardized response format with success/error/meta +- OpenAPI documentation at `/api-docs` (Scalar UI) +- Request validation with detailed error messages using Zod + +### Configuration +- Environment variables via dotenv +- Environment-aware defaults (development/production) +- Type-safe configuration with Zod validation +- Flexible Bcrypt cost factor tuning ### Performance - -- Database connection pooling -- Efficient query generation via Drizzle ORM -- Structured logging with Pino -- Graceful shutdown handling -- Fast runtime with Bun - -### Observability - -- Structured JSON logging -- Request ID tracking -- Health check endpoints -- Configurable log levels - -### Developer Experience - -- Clear project structure -- Type-safe database operations with TypeScript -- Comprehensive test coverage -- Docker support for development and deployment -- Bun package management - -## Roadmap - -### Current (v1.0) - -- User authentication and management -- Task management as example domain -- Basic infrastructure (config, logging, database) -- Testing infrastructure - -### Future Enhancements - -- Additional example domains -- Redis caching layer -- Message queue integration (RabbitMQ/Kafka) -- Metrics collection (Prometheus) -- Distributed tracing (OpenTelemetry) -- API versioning strategy -- GraphQL support option +- Bun's native JavaScript/TypeScript execution +- SQLite for small-footprint data storage +- Connection pooling via Drizzle +- Efficient middleware pipeline in Hono + +## Roadmap Considerations + +### Near-term (Potential Enhancements) +- Refresh token endpoint implementation +- Token revocation/blacklist mechanism +- Rate limiting middleware +- Request context propagation improvements +- Additional authentication methods (OAuth2, etc.) + +### Medium-term (Feature Expansion) +- Role-based access control (RBAC) +- User profile management +- Email verification flow +- Password reset functionality +- Audit logging for sensitive operations + +### Long-term (Architecture Evolution) +- Microservice extraction patterns +- Event-driven architecture support +- Redis integration for caching +- Message queue integration +- GraphQL API option (Hono supports GraphQL) ## Acceptance Criteria -- All endpoints must have proper error handling -- Database migrations must be idempotent -- Tests must cover critical business logic -- API documentation must be accurate -- Configuration must be environment-specific -- Security best practices must be followed +- All features must have unit tests (80%+ coverage for critical paths) +- Integration tests for database operations +- TypeScript strict mode enabled with no implicit any +- Code must pass ESLint checks +- Code must pass Prettier formatting +- Pre-commit hooks must validate changes +- Docker container must build and run successfully +- OpenAPI documentation must be up-to-date diff --git a/.agents/rules/memory-bank/tasks.md b/.agents/rules/memory-bank/tasks.md deleted file mode 100644 index d08b50f..0000000 --- a/.agents/rules/memory-bank/tasks.md +++ /dev/null @@ -1,720 +0,0 @@ -# Operational Workflows - -## Test-Driven Development (TDD) - -### TDD Cycle - -1. **Red:** Write a failing test for the desired behavior -2. **Green:** Write minimal code to make the test pass -3. **Refactor:** Improve code while keeping tests green - -### When to Write Tests - -- **Before implementing:** New features or business logic -- **Before fixing bugs:** Reproduce the bug with a test -- **After refactoring:** Ensure behavior unchanged -- **Critical paths:** Authentication, authorization, data persistence - -### Test Organization - -**Unit Tests:** - -- Location: Same directory as implementation (`.test.ts` suffix) -- Scope: Single function or method -- Dependencies: Mock external dependencies -- Examples: `src/domain/user/usecase/usecase.test.ts` - -**Integration Tests:** - -- Location: `test/integration/` -- Scope: End-to-end API flows -- Dependencies: Real database (testcontainers) -- Examples: `test/integration/api.test.ts` - -**Mock Tests:** - -- Location: `test/mock/` -- Scope: Database interactions -- Dependencies: Database mocks -- Examples: `test/mock/dbMock.test.ts` - -### Test Structure Template - -```typescript -describe("__", () => { - it("should return expected result", async () => { - // Arrange - const mockRepo = vi.mocked(createMockUserRepository()); - const useCase = new UserUseCase(mockRepo, cfg, argon2Cfg, log); - - // Setup expectations - mockRepo.getByEmail.mockResolvedValue(null); - - // Act - const result = await useCase.register(ctx, request); - - // Assert - expect(result).toBeDefined(); - expect(result.token).not.toBeEmpty(); - }); -}); -``` - -### Table-Driven Tests - -```typescript -describe("validateEmail", () => { - const testCases = [ - { name: "valid email", email: "user@example.com", wantErr: false }, - { name: "invalid format", email: "invalid", wantErr: true }, - { name: "empty", email: "", wantErr: true }, - ]; - - testCases.forEach(({ name, email, wantErr }) => { - it(name, () => { - const err = validateEmail(email); - expect(!!err).toBe(wantErr); - }); - }); -}); -``` - -### Running Tests - -**All tests:** - -```bash -bun test -``` - -**Specific file:** - -```bash -bun test src/domain/user/usecase/usecase.test.ts -``` - -**With coverage:** - -```bash -bun test --coverage -``` - -**Integration tests:** - -```bash -bun test test/integration/ -``` - -### Test Coverage Goals - -- **Critical business logic:** >90% -- **Domain use cases:** >80% -- **Handlers:** >70% -- **Infrastructure:** >60% -- **Overall:** >70% - -## Refactoring Procedures - -### When to Refactor - -- Code duplication detected -- Complex functions (>50 lines) -- God objects with too many responsibilities -- Poor naming or unclear intent -- Performance bottlenecks identified -- Adding new features becomes difficult - -### Refactoring Checklist - -- [ ] Ensure tests exist and pass -- [ ] Identify the smell/problem -- [ ] Plan the refactoring approach -- [ ] Make small, incremental changes -- [ ] Run tests after each change -- [ ] Verify behavior unchanged -- [ ] Update documentation if needed -- [ ] Commit with clear message - -### Common Refactorings - -**Extract Method:** - -- Move code to a new function -- Give it a descriptive name -- Replace original code with function call - -**Extract Interface:** - -- Identify common behavior -- Create interface with methods -- Implement interface in concrete types -- Update dependencies to use interface - -**Replace Magic Numbers:** - -- Identify constants in code -- Create named constants -- Replace numbers with constants -- Add documentation - -**Simplify Conditional:** - -- Use guard clauses -- Replace nested if-else with switch -- Extract complex conditions to named functions - -**Remove Dead Code:** - -- Identify unused code -- Remove or comment out -- Run tests to verify -- Commit removal - -### Refactoring Example - -**Before:** - -```typescript -async register(c: Context) { - const req = await c.req.json(); - if (!req.email || !req.password) { - return c.json({ error: 'Invalid request' }, 400); - } - if (!this.validator.validate(req)) { - return c.json({ error: 'Validation failed' }, 400); - } - // ... more code -} -``` - -**After:** - -```typescript -async register(c: Context) { - const req = await this.bindAndValidateRequest(c); - if (req instanceof Error) { - return this.errorResponse(c, 400, req); - } - // ... more code -} - -private async bindAndValidateRequest(c: Context) { - const req = await c.req.json(); - if (!req.email || !req.password) { - return new Error('Invalid request'); - } - if (!this.validator.validate(req)) { - return new Error('Validation failed'); - } - return req; -} -``` - -## Code Review Checklist - -### General Review - -- [ ] Code follows project coding standards -- [ ] Naming is clear and descriptive -- [ ] Functions are small and focused -- [ ] No code duplication -- [ ] Comments explain "why", not "what" -- [ ] No commented-out code left behind -- [ ] Proper error handling throughout -- [ ] Logging at appropriate levels - -### Architecture Review - -- [ ] Follows clean architecture principles -- [ ] Dependencies point inward -- [ ] Domain logic isolated from infrastructure -- [ ] Interfaces used for external dependencies -- [ ] No circular dependencies -- [ ] Proper separation of concerns - -### Security Review - -- [ ] Input validation on all user inputs -- [ ] SQL injection prevention (Drizzle handles this) -- [ ] Authentication/authorization enforced -- [ ] Sensitive data not logged -- [ ] Secrets not hardcoded -- [ ] CORS properly configured -- [ ] Rate limiting applied - -### Performance Review - -- [ ] No N+1 query problems -- [ ] Database queries optimized -- [ ] Connection pooling configured -- [ ] No unnecessary allocations -- [ ] Efficient data structures used -- [ ] Caching considered where appropriate - -### Testing Review - -- [ ] Tests added for new functionality -- [ ] Tests cover edge cases -- [ ] Tests are readable and maintainable -- [ ] Mocks used appropriately -- [ ] Test coverage adequate -- [ ] Integration tests included for API changes - -### Documentation Review - -- [ ] JSDoc comments on exported functions -- [ ] API documentation updated (OpenAPI) -- [ ] README updated if needed -- [ ] Architecture docs updated if major change -- [ ] Migration files documented - -### Specific Domain Reviews - -**User Domain:** - -- [ ] Password hashing with Argon2id -- [ ] Email uniqueness enforced -- [ ] JWT token properly generated -- [ ] User ownership verified - -**Task Domain:** - -- [ ] Task ownership verified -- [ ] Status values validated -- [ ] Priority values validated -- [ ] Due date handling correct - -**Database:** - -- [ ] Migration files created -- [ ] Drizzle schema updated -- [ ] Indexes added if needed -- [ ] Foreign keys defined - -## Debugging Protocols - -### Debugging Workflow - -1. **Reproduce the Issue** - - Get exact steps to reproduce - - Identify affected environment - - Gather error messages and logs - - Note request/response data - -2. **Gather Information** - - Check application logs - - Review database state - - Examine request/response - - Check configuration values - -3. **Formulate Hypothesis** - - Based on symptoms - - Consider recent changes - - Review related code - - Check known issues - -4. **Test Hypothesis** - - Add logging to verify - - Write reproduction test - - Use debugger if needed - - Isolate the problem - -5. **Implement Fix** - - Write minimal fix - - Add tests for fix - - Verify fix works - - Check for side effects - -### Debugging Tools - -**Logging:** - -```typescript -logger.debug("Processing request", { userId, taskId }); -logger.error("Failed to update task", { error: err, taskId }); -``` - -**Structured Logging:** - -- Include request ID in all logs -- Use consistent field names -- Log at appropriate levels -- Include context for errors - -**Error Inspection:** - -```typescript -if (err) { - logger.error("Operation failed", { - error: err.message, - operation: "createUser", - email: req.email, - }); - // Use instanceof for error checking -} -``` - -**Database Debugging:** - -```bash -# Connect to database -psql -h localhost -U postgres -d postgres - -# Check recent queries -SELECT * FROM pg_stat_statements ORDER BY total_time DESC LIMIT 10; - -# Check connection pool -SELECT * FROM pg_stat_activity; -``` - -**HTTP Debugging:** - -```bash -# Check API endpoint -curl -X GET http://localhost:3000/health - -# With authentication -curl -X GET http://localhost:3000/api/v1/users \ - -H "Authorization: Bearer " - -# Verbose output -curl -v http://localhost:3000/api/v1/tasks -``` - -### Common Issues & Solutions - -**Database Connection Issues:** - -- Check database is running -- Verify connection string -- Check connection pool settings -- Review firewall rules - -**Authentication Failures:** - -- Verify JWT secret matches -- Check token expiration -- Validate token format -- Review middleware configuration - -**Performance Issues:** - -- Check database query performance -- Review connection pool settings -- Profile with Bun's built-in tools -- Check for N+1 queries - -**Test Failures:** - -- Run tests with verbose output -- Check test data setup -- Verify mock expectations -- Review test isolation - -### Adding Debug Logging - -**Before Production:** - -```typescript -async login(ctx: Context, req: LoginUser) { - logger.debug('Login attempt', { email: req.email }); - - const userModel = await this.repo.getByEmail(ctx, req.email); - if (!userModel) { - logger.error('User not found', { email: req.email }); - throw ErrInvalidCredentials; - } - - // ... rest of code -} -``` - -**Remove Before Production:** - -- Remove debug-level logs -- Keep error and warn logs -- Ensure no sensitive data in logs - -### Performance Debugging - -**Enable Profiling:** - -```typescript -// Bun has built-in profiling -// Add to routes -app.get("/debug/pprof/*", async (c) => { - // Profiling endpoint -}); -``` - -### Integration Testing Debugging - -**Run Single Test:** - -```bash -bun test test/integration/api.test.ts -t "Login" -``` - -**Keep Database Running:** - -```bash -# Testcontainers handles cleanup automatically -``` - -**View Test Database:** - -```bash -# Get container ID -docker ps - -# Connect to test database -docker exec -it psql -U postgres -d postgres -``` - -## Adding a New Domain - -### Step-by-Step Process - -1. **Create Domain Structure** - - ``` - src/domain// - entity/ - handler/ - repository/ - usecase/ - request/ - response/ - mock/ - interface.ts - ``` - -2. **Define Entity** - - Create entity in `entity/.ts` - - Add UUID primary key - - Add timestamps (created_at, updated_at) - - Add business logic methods - -3. **Create Interface** - - Define Repository, Service, Handler interfaces - - Follow existing patterns - - Use domain-specific types - -4. **Implement Repository** - - Create Drizzle schema in `src/drizzle/schema/` - - Run `bunx drizzle-kit generate` to create migrations - - Implement repository interface - - Handle errors appropriately - -5. **Implement UseCase** - - Create business logic - - Define domain-specific errors - - Implement validation rules - - Add logging - -6. **Implement Handler** - - Create HTTP handlers - - Map request/response DTOs - - Handle errors - - Register routes - -7. **Add Tests** - - Unit tests for usecase - - Integration tests for API - - Mock tests for repository - -8. **Update Application** - - Wire dependencies in `app.ts` - - Register routes - - Update OpenAPI documentation - -9. **Update Documentation** - - Add to architecture.md - - Update context.md - - Add API examples - -## Database Migration Workflow - -### Creating a Migration - -1. **Create Migration File** - - ```bash - # Drizzle Kit generates migrations - bunx drizzle-kit generate - ``` - -2. **Write Migration SQL** - - Generated in `src/drizzle/migrations/` - - Review and adjust if needed - - Add indexes for foreign keys - -3. **Apply Migration** - - ```bash - bunx drizzle-kit migrate - ``` - -4. **Generate Types** - ```bash - bunx drizzle-kit generate - ``` - -### Migration Best Practices - -- Always review generated migrations -- Use transactions for complex changes -- Add indexes for foreign keys -- Consider data migration for schema changes -- Test migrations on development first -- Never modify existing migrations - -## Running the Application - -### Development - -```bash -# Set environment -export NODE_ENV=local - -# Run with hot reload -bun run dev - -# Or standard run -bun run src/index.ts -``` - -### Production - -```bash -# Build -bun build src/index.ts --outdir ./dist - -# Run -bun dist/index.js -``` - -### Docker - -```bash -# Build image -docker build -t zercle-bun-template . - -# Run container -docker run -p 3000:3000 \ - -e NODE_ENV=prod \ - -e DATABASE_URL=... \ - zercle-bun-template -``` - -### Docker Compose - -```bash -# Start all services -docker-compose up -d - -# View logs -docker-compose logs -f - -# Stop services -docker-compose down -``` - -## Common Commands - -### Linting - -```bash -# Run linter -bunx eslint . - -# Fix issues -bunx eslint . --fix -``` - -### Formatting - -```bash -# Format code -bunx prettier --write . - -# Check formatting -bunx prettier --check . -``` - -### Dependencies - -```bash -# Install dependencies -bun install - -# Update dependencies -bun update - -# Add dependency -bun add - -# Add dev dependency -bun add -d -``` - -### Documentation - -```bash -# Generate OpenAPI docs -bunx drizzle-kit studio - -# View API docs -# Navigate to http://localhost:3000/docs -``` - -### Drizzle Kit - -```bash -# Generate migrations -bunx drizzle-kit generate - -# Apply migrations -bunx drizzle-kit migrate - -# Open Drizzle Studio -bunx drizzle-kit studio -``` - -## Environment Setup - -### Prerequisites - -- Bun 1.0.0+ -- PostgreSQL 12+ -- Docker (optional, for containerized deployment) - -### Local Development - -1. Clone repository -2. Copy `.env.example` to `.env` -3. Configure database connection -4. Run migrations -5. Start application - -### Database Setup - -```bash -# Start PostgreSQL with Docker -docker run --name postgres \ - -e POSTGRES_USER=postgres \ - -e POSTGRES_PASSWORD=postgres \ - -e POSTGRES_DB=postgres \ - -p 5432:5432 \ - -d postgres:15 - -# Run migrations -bunx drizzle-kit migrate -``` - -### Seed Data - -```bash -# Run seed script -bun run scripts/seed-db.ts -``` diff --git a/.agents/rules/memory-bank/tech.md b/.agents/rules/memory-bank/tech.md index 37e6eb6..2e62c17 100644 --- a/.agents/rules/memory-bank/tech.md +++ b/.agents/rules/memory-bank/tech.md @@ -1,442 +1,315 @@ -# Technical Standards & Guidelines +# Technology Stack -## Language & Runtime +**Last Updated:** 2026-02-10 -- **TypeScript Version:** 5.x -- **Bun Version:** 1.0.0+ -- **Module:** github.com/zercle/zercle-bun-template +## Core Technologies -## Core Dependencies +### Runtime & Language +- **Bun:** 1.x (latest stable) + - Native JavaScript/TypeScript runtime + - Built-in bundler, test runner, package manager + - 3-4x faster than Node.js for many workloads + - Native TypeScript support without transpilation +- **TypeScript:** 5.x + - Strict mode enabled + - Full null checking + - Explicit type annotations required ### Web Framework - -- **Hono** - HTTP server framework -- **Hono middleware** - Request ID, logger, recovery, CORS +- **Hono.js:** 4.x + - Lightweight, portable web framework + - Middleware support (built-in and custom) + - Built for Edge, Serverless, and Node.js + - Excellent performance characteristics + - Type-safe routing and handlers ### Database - -- **pg (node-postgres)** - PostgreSQL driver -- **Drizzle ORM** - Type-safe SQL query generation -- **Drizzle Kit** - Migration and schema management +- **Better-SQLite3:** Synchronous SQLite driver + - Native bindings for performance + - Simple API + - No connection pooling needed (single connection) +- **Drizzle ORM:** 0.x + - Type-safe SQL query builder + - Lightweight (no runtime overhead) + - Migration support with Drizzle Kit + - SQLite schema definition ### Authentication - -- **jose** - JWT token generation and validation -- **argon2** - Password hashing - -### Configuration - -- **dotenv/config** - Configuration management -- **zod** - Configuration validation - -### Logging - -- **pino** - Structured, zero-allocation logging +- **JWT:** jose or jsonwebtoken + - Access tokens (15 min default) + - Refresh tokens (7 days default) + - ES256/HS256 signing +- **Bcrypt:** bcryptjs + - Password hashing + - Configurable cost factor + - Constant-time comparison ### Validation +- **Zod:** 3.x + - Type-safe schema validation + - Works seamlessly with TypeScript + - Custom validators support + - Detailed error messages -- **zod** - Request validation and schema validation - -### Documentation +### Logging +- **Bun's Built-in Logger:** + - `console.log`, `console.error`, etc. + - Performance optimized + - JSON output support in production +- **Pino.js:** (optional) + - Structured logging + - Child loggers for context + - Multiple output formats -- **OpenAPI** - API documentation generation +### Configuration +- **Dotenv:** 16.x + - Environment variable loading + - Support for .env files + - Type-safe parsing with Zod + +## Development Tools + +### Code Quality +- **ESLint:** Linting with TypeScript support + - Configured with recommended rules + - Hono.js plugin + - Prettier integration +- **Prettier:** Code formatting + - Consistent style across codebase + - TypeScript-aware formatting + +### API Documentation +- **Scalar:** OpenAPI documentation + - Beautiful API docs UI + - OpenAPI 3.0 support + - Available at `/api-docs` +- **TypeDoc:** (optional) API reference + - Generate documentation from code + - Support for TypeScript features ### Testing - -- **bun test** - Built-in testing framework -- **vi (Vitest)** - Mocking utilities -- **testcontainers** - Integration testing - -## Coding Standards - -### Naming Conventions - -- **Files:** camelCase with .ts extension (e.g., `userHandler.ts`) -- **Packages/Directories:** lowercase (e.g., `handler`, `usecase`) -- **Interfaces:** PascalCase with 'I' prefix (e.g., `IUserRepository`) -- **Classes:** PascalCase (e.g., `UserUseCase`, `UserHandler`) -- **Constants:** UPPER_SNAKE_CASE -- **Private variables:** camelCase with underscore prefix (e.g., `_privateVar`) -- **Public variables:** camelCase - -### Code Organization - -- **Directory structure:** One responsibility per directory -- **File size:** Keep files focused and under 300 lines when possible -- **Function length:** Prefer functions under 50 lines -- **Exported functions:** Must have JSDoc comments -- **Error handling:** Always handle errors, never ignore - -### TypeScript Specifics - -- **Strict mode enabled:** All TypeScript strict checks -- **Explicit types:** Avoid `any` type, use `unknown` for truly unknown data -- **Interfaces vs Types:** Use interfaces for object shapes, types for unions/intersections -- **Enums:** Prefer string enums or const assertions -- **Async/await:** Prefer over Promises for readability - -### Design Patterns - -**Repository Pattern:** - -- Abstract data access behind interfaces -- Domain entities mapped to database models -- Repository implementations in infrastructure layer - -**Use Case Pattern:** - -- Business logic encapsulated in use cases -- Coordinate between repositories and handlers -- Domain-specific error definitions - -**Factory Pattern:** - -- Database factory for creating connections -- Configuration-based instantiation - -**Middleware Pattern:** - -- Request/response processing pipeline -- Cross-cutting concerns (auth, logging, CORS) - -### SOLID Principles - -**Single Responsibility:** - -- Each module has one clear purpose -- Functions do one thing well -- Classes/interfaces focused on single capability - -**Open/Closed:** - -- Interfaces for extensibility -- New features through new implementations -- Avoid modifying existing, stable code - -**Liskov Substitution:** - -- Interface contracts honored by implementations -- Mock implementations behave like real ones - -**Interface Segregation:** - -- Small, focused interfaces -- Clients depend only on needed methods - -**Dependency Inversion:** - -- Depend on abstractions (interfaces) -- High-level modules don't depend on low-level -- Inversion of Control through DI - -## Testing Guidelines - -### Test Structure - -- **Unit tests:** Test individual functions/methods -- **Integration tests:** Test component interactions -- **Table-driven tests:** Multiple test cases in one function -- **Mock tests:** Use generated mocks for dependencies - -### Test Organization - +- **Bun Test:** Built-in test runner + - Jest-compatible API + - Fast execution + - Coverage reporting + - Mocking support +- **Supertest:** HTTP testing + - Test Hono handlers + - Request/response inspection + +### Type Generation +- **Drizzle Kit:** Migration generation + - Generate migrations from schema + - Push schema to database + - Type-safe migrations + +### Package Management +- **Bun:** Native package manager + - Fast installs + - Lockfile support + - Workspaces support + +## Dependencies Summary + +### Core Dependencies ``` -src/ - domain/ - user/ - handler/ - handler.ts - handler.test.ts - usecase/ - usecase.ts - usecase.test.ts -test/ - integration/ - api.test.ts - mock/ - dbMock.test.ts +hono +drizzle-orm +better-sqlite3 +drizzle-kit +bcryptjs +jose +zod +dotenv ``` -### Testing Best Practices - -- Write tests for critical business logic -- Aim for >80% coverage on core paths -- Use table-driven tests for multiple scenarios -- Mock external dependencies (database, HTTP clients) -- Use testcontainers for real database integration tests -- Test error paths, not just happy paths - -### Test Naming - -- `describe('__')` -- `it('should ')` -- Example: `describe('Login_ValidCredentials_ReturnsToken')` - -## Security Standards - -### Password Storage - -- Always use Argon2 for password hashing -- Configurable memory, iterations, parallelism -- Never store plaintext passwords - -### Authentication - -- JWT tokens for stateless authentication -- Token expiration configurable -- Secret key must be environment-specific -- Validate tokens on protected routes - -### Input Validation - -- Validate all user inputs -- Use Zod schemas for request DTOs -- Sanitize database queries (Drizzle prevents SQL injection) -- Validate file uploads (size, type) - -### CORS Configuration - -- Whitelist allowed origins per environment -- Configure allowed methods and headers -- Use secure defaults for production - -### Rate Limiting - -- Configurable requests per time window -- Apply to API endpoints -- Prevent abuse and DoS attacks - -## Database Standards - -### Migrations - -- Use Drizzle Kit migration format -- Up and down migrations required -- Version with timestamp format -- Place in `src/drizzle/migrations/` directory - -### Queries - -- Use Drizzle ORM for type-safe queries -- Schema definitions in `src/drizzle/schema/` -- Named queries for clarity -- Parameterized queries (Drizzle handles this) - -### Connection Pooling - -- Configure min/max connections -- Set connection lifetime and idle timeout -- Health check period for stale connections -- Adjust based on application load - -## Error Handling - -### Error Types - -- **Domain errors:** Business rule violations (e.g., `ErrUserNotFound`) -- **Repository errors:** Data access failures -- **Infrastructure errors:** External service failures -- **Validation errors:** Input validation failures - -### Error Wrapping - -- Wrap errors with context using custom error classes -- Use `instanceof` for error checking -- Log errors with sufficient context -- Return appropriate HTTP status codes - -### HTTP Status Codes - -- 200 OK - Successful GET/PUT/PATCH -- 201 Created - Successful POST -- 400 Bad Request - Validation errors -- 401 Unauthorized - Missing/invalid JWT -- 404 Not Found - Resource not found -- 409 Conflict - Duplicate resources -- 500 Internal Server Error - Unexpected errors - -## Logging Standards - -### Log Levels - -- **Debug:** Detailed diagnostic information -- **Info:** General informational messages -- **Warn:** Warning messages for potential issues -- **Error:** Error events that might still allow continued operation -- **Fatal:** Severe errors requiring immediate attention - -### Log Format - -- Structured JSON logging -- Include request ID for tracing -- Contextual fields (user_id, action, resource) -- Timestamps in ISO 8601 format - -### What to Log - -- Application startup/shutdown -- Request/response for API calls (with request ID) -- Errors with stack traces -- Business events (user registration, task creation) -- Performance metrics (slow queries, long-running operations) - -## API Standards - -### RESTful Design - -- Use appropriate HTTP methods (GET, POST, PUT, PATCH, DELETE) -- Resource-based URLs (e.g., `/api/v1/users/:id`) -- Query parameters for filtering and pagination -- Consistent response format - -### Response Format +### Development Dependencies +``` +typescript +bun-types +eslint +prettier +@typescript-eslint/eslint-plugin +@typescript-eslint/parser +eslint-config-prettier +eslint-plugin-prettier +``` -```json -{ - "data": { ... }, - "error": null, - "meta": { "total": 100, "page": 1 } +## Configuration Management + +### Configuration Sources (Priority Order) +1. Runtime environment variables (highest) +2. .env file +3. Default values (lowest) + +### Environment Variables Prefix +- Prefix: `APP_` +- Example: `APP_SERVER_PORT=3000` + +### Configuration Structure +```typescript +// src/core/config.ts +interface Config { + app: { + name: string; + version: string; + environment: string; + }; + server: { + host: string; + port: number; + }; + database: { + enabled: boolean; + path: string; + }; + log: { + level: 'debug' | 'info' | 'warn' | 'error'; + }; + jwt: { + secret: string; + accessTokenTtl: string; + refreshTokenTtl: string; + }; + security: { + bcryptCost: number; + }; } ``` -### Versioning - -- URL-based versioning: `/api/v1/` -- Backward compatibility within major versions -- Deprecation notices for breaking changes - -### Documentation - -- OpenAPI/Swagger documentation -- Auto-generated from code annotations -- Example requests/responses -- Authentication requirements documented - -## Deployment Guidelines +## Deployment Configuration ### Docker +- **Dockerfile:** Multi-stage build with Bun +- **Docker Compose:** Test environment setup +- **.dockerignore:** Exclude unnecessary files + +### Database Migrations +- **Location:** `src/infrastructure/db/migrations/` +- **Format:** Drizzle migration files +- **Tool:** Drizzle Kit CLI + +### Connection Settings +- SQLite: File-based (path configurable) +- Connection: Single synchronous connection +- WAL mode: Enabled by default for performance + +## Security Configuration + +### Bcrypt Defaults +- **Production:** Cost = 12 +- **Development:** Cost = 4 (faster, not for production) + +### JWT Configuration +- Signing method: HS256 (configurable) +- Access token TTL: 15 minutes (configurable) +- Refresh token TTL: 7 days (configurable) +- Issuer: zercle-hono-template + +## Testing Strategy + +### Test Types +1. **Unit Tests:** Isolated component testing +2. **Integration Tests:** Database interaction testing +3. **E2E Tests:** Full API testing (optional) + +### Test Coverage Goal +- 80%+ for critical paths +- All business logic covered +- All handlers covered + +### Test Naming Convention +- Unit: `*.test.ts` +- Integration: `*.integration.test.ts` +- Test files alongside source files + +## Code Standards + +### TypeScript Conventions +- Strict mode enabled +- No implicit `any` +- Explicit return types +- No `any` casts +- Full null checking -- Multi-stage builds for optimization -- Alpine-based images for smaller size -- Non-root user for security -- Health checks defined in Dockerfile - -### Configuration - -- Environment-specific configs (local, dev, uat, prod) -- Sensitive data via environment variables -- Never commit secrets to repository - -### Health Checks - -- `/health` - Application health -- `/readiness` - Readiness for traffic -- Database connectivity check -- Dependency service checks - -## Performance Guidelines - -### Database - -- Use connection pooling -- Optimize queries with proper indexes -- Batch operations when possible -- Use prepared statements (Drizzle handles this) - -### HTTP - -- Enable compression for large responses -- Use appropriate cache headers -- Implement rate limiting -- Monitor response times - -### Memory - -- Reuse objects where possible -- Avoid allocations in hot paths -- Use value types for small structs -- Profile before optimizing - -## Code Quality - -### Linting - -- Use ESLint with TypeScript support -- Configure in `.eslintrc.json` -- Run in CI/CD pipeline - -### Formatting - -- Use Prettier for consistent formatting -- Configure in `.prettierrc` -- Auto-format on save - -### Code Review Checklist - -- Follows coding standards -- Tests included and passing -- Error handling complete -- Documentation updated -- No security vulnerabilities -- Performance considered - -### Documentation - -- JSDoc comments for exported functions -- README with setup instructions -- API documentation (OpenAPI) -- Architecture documentation (Memory Bank) - -## TypeScript Best Practices - -### Type Safety - -- Enable strict mode in tsconfig.json -- Use explicit return types for functions -- Avoid `any` type -- Use `unknown` for truly unknown data -- Leverage type inference where appropriate - -### Async Patterns - -- Use async/await over Promises -- Handle errors with try/catch -- Use Promise.all for parallel operations -- Consider AbortController for cancellation - -### Module System - -- Use ES modules (import/export) -- Avoid default exports -- Use named exports for better tree-shaking -- Keep barrel files (index.ts) for clean imports +### Naming Conventions +- Interfaces: PascalCase with `I` prefix (e.g., `IUserRepository`) +- Types: PascalCase (e.g., `UserResponse`) +- Variables/Functions: camelCase +- Constants: SCREAMING_SNAKE_CASE +- Files: kebab-case (e.g., `user-repository.ts`) ### Error Handling +- Wrap errors with context +- Use typed errors where appropriate +- Log all errors with context +- Provide user-friendly messages +- Use Zod for input validation errors + +### Import Organization +1. External libraries +2. Internal modules (absolute imports) +3. Relative imports (sorted) + +## Performance Considerations + +### Optimization Strategies +- Bun's native execution (no transpilation) +- SQLite with WAL mode +- Lazy initialization where appropriate +- Efficient data structures +- Proper indexing in database + +### Monitoring Points +- Request latency +- Database query performance +- Memory usage +- Error rates +- Token generation performance + +### Bun-specific Optimizations +- Use `Bun.file()` for file operations +- Use `Bun.write()` for fast writes +- Leverage `Bun.inspect()` for debugging +- Use `Bun.serve()` for production deployment + +## Project Structure -- Create custom error classes -- Use error codes for internationalization -- Include context in error messages -- Don't expose sensitive data in errors - -## Bun Specific Guidelines - -### Performance - -- Leverage Bun's fast startup time -- Use Bun's built-in test runner -- Take advantage of Bun's native APIs -- Use Bun's file system API for I/O - -### Compatibility - -- Ensure Node.js compatibility when needed -- Use Bun's polyfills for web APIs -- Test in both Bun and Node.js environments -- Be aware of Bun-specific APIs - -### Development - -- Use `bun run` for scripts -- Use `bun install` for dependencies -- Use `bun test` for testing -- Use `bun build` for production builds +``` +zercle-hono-template/ +├── src/ +│ ├── app.ts # Application entry +│ ├── main.ts # Server startup +│ ├── core/ +│ │ ├── config.ts # Configuration +│ │ ├── errors.ts # Error types +│ │ └── types.ts # Shared types +│ ├── features/ +│ │ ├── auth/ +│ │ └── user/ +│ ├── infrastructure/ +│ │ ├── db/ +│ │ │ ├── schema.ts # Drizzle schema +│ │ │ ├── migrations/ # Migrations +│ │ │ └── repositories/ # Implementations +│ │ ├── di/ +│ │ └── logging/ +│ └── presentation/ +│ ├── handlers/ +│ ├── middleware/ +│ └── routes/ +├── configs/ +│ └── .env.example +├── drizzle/ +│ └── config.ts +├── tests/ +│ └── *.test.ts +├── package.json +├── tsconfig.json +├── drizzle.config.ts +├── eslint.config.js +├── prettier.config.js +├── .dockerignore +├── Dockerfile +└── docker-compose.yml +``` diff --git a/.dockerignore b/.dockerignore index fa669ce..385094b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,130 @@ +# ============================================ +# Git and Version Control +# ============================================ .git -.github -node_modules -tests -coverage -*.log +.gitignore +.gitattributes +.mdlrc +.editorconfig +.prettierignore +.prettierrc +.eslintignore +.eslintrc +.eslintrc.js +.eslintrc.json + +# ============================================ +# Development Files +# ============================================ .env -.env.* -!.env.example +.env.example +.env.local +.env.development +.env.test +.env.production +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# ============================================ +# IDE and Editor +# ============================================ +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# ============================================ +# Testing and Coverage +# ============================================ +coverage/ +.nyc_output/ +*.lcov +.htmlcov/ + +# ============================================ +# Node.js +# ============================================ +node_modules/ +dist/ +build/ +!.docker/build +!.docker/Dockerfile + +# ============================================ +# Docker +# ============================================ +docker-compose.yml +docker-compose.*.yml +Dockerfile* +.docker/ + +# ============================================ +# Documentation +# ============================================ +*.md +!README.md +LICENSE.md +ARCHITECTURE.md +CONTRIBUTING.md +CHANGELOG.md + +# ============================================ +# Cache and Temporary +# ============================================ +.cache/ +.tmp/ +.temp/ +*.tmp +*.temp + +# ============================================ +# OS Files +# ============================================ +.DS_Store +Thumbs.db +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# ============================================ +# Secrets and Credentials +# ============================================ +*.pem +*.key +*.crt +credentials.json +secrets/ +private/ + +# ============================================ +# CI/CD +# ============================================ +.github/workflows/*.yml +!.github/workflows/ci.yml + +# ============================================ +# Pre-commit and Hooks +# ============================================ +.husky/ +.pre-commit-config.yaml +.commitlintrc +.commitlintrc.js +.commitlintrc.json + +# ============================================ +# Test +# ============================================ +test/ +*.test.ts +*.test.js +*.spec.ts +*.spec.js +__tests__/ +vitest.config.ts +jest.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3001d86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{ts,tsx,js,jsx}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.{json,yaml,yml}] +indent_size = 2 diff --git a/.env.example b/.env.example index c189a67..7f20929 100644 --- a/.env.example +++ b/.env.example @@ -1,32 +1,31 @@ -# Server Configuration -SERVER_ENV=local -SERVER_PORT=3000 -SERVER_HOST=0.0.0.0 +# Application +NODE_ENV=development +PORT=3000 +HOST=0.0.0.0 +APP_NAME=Hono API +APP_VERSION=1.0.0 -# Database Configuration -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=postgres -DB_NAME=postgres -DB_DRIVER=postgres +# CORS +CORS_ORIGIN=http://localhost:3000 -# JWT Configuration -JWT_SECRET=your-secret-key-change-in-production -JWT_EXPIRATION=3600 +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/dbname +DATABASE_POOL_MIN=2 +DATABASE_POOL_MAX=10 -# Logging Configuration +# Authentication +JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters-long +JWT_EXPIRES_IN=1h + +# Logging LOG_LEVEL=info -LOG_FORMAT=json -# CORS Configuration -# CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +# Rate Limiting +RATE_LIMIT_WINDOW_MS=60000 +RATE_LIMIT_MAX_REQUESTS=100 -# Rate Limiting Configuration -RATE_LIMIT_REQUESTS=100 -RATE_LIMIT_WINDOW=60 +# Redis (optional) +# REDIS_URL=redis://localhost:6379 -# Argon2id Configuration -ARGON2ID_MEMORY=19456 -ARGON2ID_ITERATIONS=2 -ARGON2ID_PARALLELISM=1 +# External Services +# API_KEY_EXTERNAL=your-api-key-here diff --git a/.env.test b/.env.test deleted file mode 100644 index b11c4ae..0000000 --- a/.env.test +++ /dev/null @@ -1,32 +0,0 @@ -# Test Environment Configuration -# Used for integration tests with Podman PostgreSQL - -# Database Configuration -DATABASE_HOST=localhost -DATABASE_PORT=5432 -DATABASE_USER=test_user -DATABASE_PASSWORD=test_password -DATABASE_NAME=test_db -DATABASE_URL=postgres://test_user:test_password@localhost:5432/test_db - -# Test-specific Database Settings -DATABASE_MAX_CONNS=5 -DATABASE_MIN_CONNS=1 -DATABASE_MAX_CONN_LIFETIME=5m -DATABASE_MAX_CONN_IDLETIME=1m -DATABASE_HEALTH_CHECK_PERIOD=30s - -# Application Configuration (Test) -NODE_ENV=test -LOG_LEVEL=error - -# Password Hashing Configuration (Test) -PASSWORD_HASH_MEMORY=65536 -PASSWORD_HASH_ITERATIONS=3 -PASSWORD_HASH_PARALLELISM=1 -PASSWORD_HASH_SALT_LENGTH=16 -PASSWORD_HASH_KEY_LENGTH=32 - -# JWT Configuration (Test) -JWT_SECRET=test-secret-key-for-integration-tests-only -JWT_EXPIRES_IN=1h diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8488c26 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,79 @@ +version: 2 +updates: + # Bun/npm dependencies + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Bangkok" + open-pull-requests-limit: 20 + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "chore(deps)" + prefix-development: "chore(deps-dev)" + include: "scope" + ignore: [] + allow: + - dependency-type: "direct" + - dependency-type: "indirect" + groups: + development-dependencies: + patterns: + - "@types/*" + - "eslint*" + - "prettier*" + update-types: + - "minor" + - "patch" + runtime-dependencies: + patterns: + - "hono" + - "zod" + update-types: + - "minor" + - "patch" + + # GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + timezone: "Asia/Bangkok" + open-pull-requests-limit: 10 + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore(ci)" + prefix-development: "chore(ci)" + include: "scope" + ignore: [] + groups: + github-actions: + patterns: + - "*" + update-types: + - "minor" + - "patch" + + # Docker dependencies (if using Docker base images) + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" + day: "monday" + time: "09:00" + timezone: "Asia/Bangkok" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "chore(docker)" + include: "scope" diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..71561b3 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,86 @@ +changelog: + categories: + - title: 🚀 Features + labels: + - "feature" + - "enhancement" + - "feat" + - title: 🐛 Bug Fixes + labels: + - "bug" + - "fix" + - "bugfix" + - "fixes" + - title: 📝 Documentation + labels: + - "documentation" + - "docs" + - "markdown" + - title: 🎨 Code Style & Formatting + labels: + - "style" + - "formatting" + - "fmt" + - "refactor" + - title: ♻️ Refactoring + labels: + - "refactor" + - "refactoring" + - "architectural" + - title: ⚡ Performance + labels: + - "performance" + - "optimization" + - "perf" + - title: 🧪 Testing + labels: + - "test" + - "testing" + - "tests" + - "unittest" + - "integration" + - title: 🔧 Tooling & Configuration + labels: + - "tooling" + - "dependencies" + - "github-actions" + - "ci" + - "build" + - "configuration" + - "config" + - title: 🔒 Security + labels: + - "security" + - "vulnerability" + - "cve" + - title: 👻 Maintenance + labels: + - "maintenance" + - "chore" + - "cleanup" + - "deprecated" + - title: Other Changes + labels: + - "*" + exclude: + labels: + - "documentation" + - "feature" + - "bug" + - "fix" + - "test" + - "testing" + - "refactor" + - "style" + - "chore" + - "dependencies" + - "github-actions" + + exclude: + labels: + - "ignore" + - "wontfix" + - "invalid" + - "duplicate" + - "question" + - "triage" diff --git a/.github/template-cleanup.yml b/.github/template-cleanup.yml new file mode 100644 index 0000000..e79817c --- /dev/null +++ b/.github/template-cleanup.yml @@ -0,0 +1,89 @@ +name: Template Cleanup + +on: + create: + branches: + - main + - master + +permissions: + contents: read + actions: read + pull-requests: read + +jobs: + cleanup: + name: Template Cleanup + runs-on: ubuntu-latest + if: github.repository != github.event.repository.full_name + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Remove template documentation + run: | + # Remove template-specific sections from ARCHITECTURE.md + if grep -q "" ARCHITECTURE.md; then + sed -i '//,//d' ARCHITECTURE.md + fi + + # Remove template README sections if they exist + if grep -q "" README.md 2>/dev/null; then + sed -i '//,//d' README.md 2>/dev/null || true + fi + + echo "Template-specific documentation removed" + + - name: Update package.json + run: | + # Get the repository name from the event + REPO_NAME=$(basename "${{ github.event.repository.full_name }}") + + # Update package.json name + bun run --cwd . ./scripts/update-package-name.js "$REPO_NAME" || { + # Fallback: manually update package.json + sed -i "s/\"name\": \"zercle-bun-template\"/\"name\": \"$REPO_NAME\"/" package.json + } + + echo "package.json updated with repository name" + + - name: Remove .github directory template files + run: | + # Remove template-specific GitHub configuration + rm -f .github/template-cleanup.yml 2>/dev/null || true + echo "Template cleanup workflow removed" + + - name: Create initial commit + run: | + git config user.email "github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + + git add -A + git status || true + + # Only commit if there are changes + if git diff --cached --quiet; then + echo "No changes to commit" + else + git commit -m "chore: Initialize repository from template + + - Remove template-specific documentation + - Update package.json with repository name + - Configure for new repository use" + + git push origin HEAD || echo "Push failed (may not have permissions)" + fi + + - name: Summary + run: | + echo "Template cleanup completed successfully" + echo "Repository: ${{ github.event.repository.full_name }}" + echo "Default branch: ${{ github.event.repository.default_branch }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 122bee3..362ce61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,36 +2,36 @@ name: CI on: push: - branches: [main, develop] + branches: [main, master, develop] pull_request: - branches: [main, develop] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - security-events: write - pull-requests: read + branches: [main, master, develop] env: - BUN_VERSION: "latest" - POSTGRES_VERSION: "18" + BUN_VERSION: "1" + NODE_ENV: test jobs: lint: name: Lint runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: bun-version: ${{ env.BUN_VERSION }} - cache: true + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + key: ${{ runner.os }}-bun-lint-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun-lint- - name: Install dependencies run: bun install @@ -39,166 +39,92 @@ jobs: - name: Run ESLint run: bun run lint - security: - name: Security Scan + typecheck: + name: TypeCheck runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: bun-version: ${{ env.BUN_VERSION }} - cache: true - - - name: Install dependencies - run: bun install - - name: Run Bun Audit - run: bun audit - - format: - name: Check Format - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Cache Bun dependencies + uses: actions/cache@v4 with: - bun-version: ${{ env.BUN_VERSION }} - cache: true + path: | + ~/.bun/install/cache + node_modules + key: ${{ runner.os }}-bun-typecheck-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun-typecheck- - name: Install dependencies run: bun install - - name: Check code formatting - run: bun run format:check + - name: Run TypeScript type checking + run: bun run typecheck - generate: - name: Code Generation + test: + name: Test runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: bun-version: ${{ env.BUN_VERSION }} - cache: true - - name: Install dependencies - run: bun install - - - name: Generate database code - run: bun run db:generate - - - name: Check for uncommitted changes - run: | - git diff --exit-code || (echo "Generated code is out of date. Run 'bun run db:generate' and commit the changes."; exit 1) - - test-unit: - name: Unit Tests - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Cache Bun dependencies + uses: actions/cache@v4 with: - bun-version: ${{ env.BUN_VERSION }} - cache: true + path: | + ~/.bun/install/cache + node_modules + key: ${{ runner.os }}-bun-test-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun-test- - name: Install dependencies run: bun install - - name: Run unit tests - run: bun test - - test-integration: - name: Integration Tests - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Run tests + run: bun run test - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Upload coverage + uses: codecov/codecov-action@v4 + if: success() with: - bun-version: ${{ env.BUN_VERSION }} - cache: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Install dependencies - run: bun install - - - name: Start test database - run: docker compose -f deployments/docker/docker-compose.test.yml up -d - - - name: Wait for database to be ready - run: | - for i in $(seq 1 60); do - if docker inspect --format='{{.State.Health.Status}}' zercle-test-db 2>/dev/null | grep -q "healthy"; then - echo "Database is ready" - exit 0 - fi - sleep 1 - done - echo "Database did not become ready in time" - exit 1 - - - name: Run integration tests - run: bun run test:integration - - - name: Stop and clean up test database - run: docker compose -f deployments/docker/docker-compose.test.yml down -v - - test-coverage: - name: Test Coverage - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v6 - - - name: Set up Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: ${{ env.BUN_VERSION }} - cache: true - - - name: Install dependencies - run: bun install - - - name: Run tests with coverage - run: bun run test:coverage - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 - with: - files: ./coverage.lcov - flags: unittests - name: codecov-umbrella + files: ./coverage/lcov.info fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} build: name: Build runs-on: ubuntu-latest - needs: [lint, format, generate, test-unit, test-integration, security] + needs: [lint, typecheck, test] steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v4 - - name: Set up Bun - uses: oven-sh/setup-bun@v2 + - name: Setup Bun + uses: oven-sh/setup-bun@v1 with: bun-version: ${{ env.BUN_VERSION }} - cache: true + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + ~/.bun/install/cache + node_modules + key: ${{ runner.os }}-bun-build-${{ hashFiles('bun.lockb') }} + restore-keys: | + ${{ runner.os }}-bun-build- - name: Install dependencies run: bun install @@ -210,22 +136,45 @@ jobs: uses: actions/upload-artifact@v4 with: name: dist - path: dist/ + path: dist retention-days: 7 docker: name: Docker Build runs-on: ubuntu-latest - needs: [lint, format, generate, test-unit, test-integration, security] + needs: [lint, typecheck, test] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') steps: - - name: Checkout code - uses: actions/checkout@v6 + - name: Checkout repository + uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Build Docker image - run: docker compose -f deployments/docker/docker-compose.yml build + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest + type=sha,prefix= - - name: List Docker images - run: docker images + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUN_VERSION=${{ env.BUN_VERSION }} diff --git a/.gitignore b/.gitignore index 40e8592..a42d3d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,85 @@ +# ============================================================================= # Dependencies +# ============================================================================= node_modules/ -.pnp -.pnp.js - -# Testing -coverage/ +.bun-cache/ +bun.lockb -# Build output +# ============================================================================= +# Build outputs +# ============================================================================= dist/ build/ +*.tsbuildinfo -# Environment variables +# ============================================================================= +# Environment files +# ============================================================================= .env .env.local .env.*.local +!.env.example +# ============================================================================= # Logs +# ============================================================================= logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* -# OS -.DS_Store -Thumbs.db +# ============================================================================= +# Testing +# ============================================================================= +coverage/ +.nyc_output/ +# ============================================================================= # IDE -.vscode/ +# ============================================================================= .idea/ +.vscode/ *.swp *.swo *~ +.DS_Store -# Bun -bun.lockb +# ============================================================================= +# OS +# ============================================================================= +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# ============================================================================= +# Temporary files +# ============================================================================= +*.tmp +*.temp +.cache/ +# ============================================================================= # Database +# ============================================================================= *.db *.sqlite +*.sqlite3 + +# ============================================================================= +# Docker +# ============================================================================= +docker-compose.override.yml + +# ============================================================================= +# Misc +# ============================================================================= +*.pid +*.seed +*.pid.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..89a2634 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,98 @@ +# Pre-commit hooks configuration for Hono.js/Bun Template +# Install pre-commit: pip install pre-commit or brew install pre-commit +# Install hooks: make hooks-install +# Run manually: make hooks-run + +repos: + - repo: local + hooks: + - id: eslint + name: ESLint + description: Lint TypeScript/JavaScript code with ESLint + entry: bun run lint + language: system + types: [ts, tsx, js, jsx] + pass_filenames: false + always_run: true + + - id: prettier + name: Prettier + description: Format code with Prettier + entry: bun run format + language: system + types: [ts, tsx, js, jsx, json, yaml, yml, md] + pass_filenames: false + always_run: true + + - id: typecheck + name: Type Check + description: Run TypeScript type checking + entry: bun run typecheck + language: system + types: [ts, tsx] + pass_filenames: false + always_run: true + + - id: test + name: Run Tests + description: Run unit tests with Bun + entry: bun test --run + language: system + pass_filenames: false + always_run: true + + - id: db-migrations + name: Database Migrations + description: Generate Drizzle ORM migrations + entry: bun run db:generate + language: system + types: [ts] + pass_filenames: false + always_run: true + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-added-large-files + args: ['--maxkb=500'] + - id: detect-private-key + + - repo: https://github.com/sindreSorhus/eslint-plugin-unicorn + rev: v56.0.1 + hooks: + - id: eslint-plugin-unicorn + name: ESLint Unicorn + description: Enforce consistent and practical JavaScript code + entry: bun run lint + language: system + types: [ts, tsx, js, jsx] + pass_filenames: false + always_run: true + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v9.19.0 + hooks: + - id: eslint + name: ESLint (mirrored) + description: Run ESLint on TypeScript/JavaScript files + entry: bun run lint + language: system + types: [ts, tsx, js, jsx] + pass_filenames: true + always_run: false + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.4.2 + hooks: + - id: prettier + name: Prettier (mirrored) + description: Format code with Prettier + entry: bun run format + language: system + types: [ts, tsx, js, jsx, json, yaml, yml, md] + pass_filenames: true + always_run: false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..71f2f1e --- /dev/null +++ b/.prettierignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ + +# Build and dist +dist/ +build/ +*.bun + +# Coverage +coverage/ +*.lcov + +# Logs +*.log +logs/ + +# Environment files +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Generated files +*.min.js +*.min.css + +# Test artifacts +test-results/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..85d3dcb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "plugins": [] +} diff --git a/AGENTS.md b/AGENTS.md index 0c277e3..6a62646 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,6 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan # Memory Bank System ## Files (`.agents/rules/memory-bank/`) - - `brief.md` - project overview (read-only) - `product.md` - goals, UX, features, roadmap, acceptance criteria - `architecture.md` - architecture, components, data flows, integrations, scalability @@ -16,7 +15,6 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan - `tasks.md` - workflows (TDD, refactoring, reviews, debugging) ## Startup - 1. Load all files: brief → product → architecture → tech → context → tasks 2. Print `Memory Bank: Active` (count, timestamps) or `Memory Bank: Missing` (list files) 3. Summarize project (3–6 bullets): purpose, architecture, tech, features, state, constraints @@ -24,13 +22,11 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan 5. Validate consistency; report conflicts immediately ## Rules - **Reference before:** architecture changes, naming, implementation, coding standards, testing, refactoring, reviews, security, performance **Scan only when:** user commands `update memory bank`, `initialize memory bank`, `audit codebase`, or confirms `OK to update Memory Bank?` **Updates:** - - Extract long-term knowledge only (no raw code, no ephemeral details) - Update `context.md` first, cascade to others - Confirm before overwriting >50 lines or critical decisions @@ -39,7 +35,6 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan - Log reversed/changed decisions in `decisions_log.md` **Permissions:** - - Editable: `context.md`, `architecture.md`, `tech.md`, `product.md`, `tasks.md` - Read-only: `brief.md` (requires approval) - Structural changes need explicit approval @@ -51,7 +46,6 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan # Programming Guidelines ## Performance & Concurrency - - Object pooling, preallocate collections, optimize cache locality - Minimize heap allocations; prefer stack for short-lived objects - Zero-copy patterns, controlled thread/worker pools, atomic ops @@ -60,39 +54,33 @@ description: AI Coder Unified Ruleset - Concise memory bank system + coding stan - Profile before optimizing; measure improvements ## I/O & Memory - - Buffered I/O, batch DB operations, connection pooling - Respect platform memory limits, reuse hot-path objects - Use value types for critical paths, proper disposal patterns - Stream large data, compress at rest/transit, circuit breakers ## Core Principles - - **SOLID:** SRP, OCP, LSP, ISP, DIP - **Generics:** type-safe reuse, constraints, cross-component interfaces - **Idiomatic:** small focused functions (<50 lines), descriptive names, DI over global state, composition > inheritance - **Error Handling:** typed errors with context, domain wrapping, recovery paths, detailed logging, explicit error surface ## Testing & Patterns - - **Tests:** unit (logic), integration (interactions), table-driven (scenarios), benchmarks (performance), mocks/stubs, property-based; 80%+ coverage for critical paths - **Patterns:** Factory, Strategy, Observer, Decorator, Command, Template Method, Builder, Repository, Singleton (use DI), Adapter ## Anti‑Patterns - - Excessive complexity, god interfaces, silent errors, overuse of reflection - Tight coupling, premature optimization, magic numbers, duplication - Feature bloat, circular dependencies, deep nesting (early returns preferred) ## Additional - - Design for composition/extensibility, strong typing, clear interface docs - Consistent naming, pure functions where appropriate, proper logging - Security best practices (validation, encoding, least privilege) - Readable/maintainable > clever code, meaningful commits, document non-obvious decisions # Agent Behavior - - Load Memory Bank at task start; use as primary context - Follow guidelines; verify alignment before implementation - Never read source files unless user requests diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ff24c5a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,152 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +## [Unreleased] + +### Added + +- Initial project structure and documentation +- Base Hono.js application with TypeScript +- Docker configuration for containerized deployment +- GitHub Actions CI/CD pipeline +- ESLint and Prettier configuration +- Health check endpoints +- Error handling middleware +- Request logging middleware +- Comprehensive test suite +- Development and deployment documentation + +### Changed + +- N/A + +### Deprecated + +- N/A + +### Removed + +- N/A + +### Fixed + +- N/A + +### Security + +- N/A + +--- + +## [0.1.0] - 2024-01-15 + +### Added + +- **Core Application** + - Hono.js web framework integration + - TypeScript configuration with strict mode + - Bun runtime support + +- **Middleware** + - Error handling middleware with proper HTTP status codes + - Request/response logging middleware + - 404 not-found handler + +- **API Endpoints** + - Health check endpoint (`GET /health`) + - Readiness probe (`GET /health/ready`) + - Liveness probe (`GET /health/live`) + - API root endpoint (`GET /api`) + +- **Testing** + - Bun test runner configuration + - Health check tests + - Middleware tests + - Test setup utilities + +- **Documentation** + - README.md with quick start guide + - CONTRIBUTING.md with contribution guidelines + - ARCHITECTURE.md with technical details + - DEVELOPMENT.md with development guide + - API.md with endpoint documentation + - DEPLOYMENT.md with deployment instructions + - SECURITY.md with security policy + +- **DevOps** + - Dockerfile for containerized deployment + - docker-compose.yml for local development + - GitHub Actions workflow for CI/CD + - Pre-commit hooks configuration + +### Changed + +- N/A (initial release) + +### Deprecated + +- N/A (initial release) + +### Removed + +- N/A (initial release) + +### Fixed + +- N/A (initial release) + +### Security + +- N/A (initial release) + +--- + +## Template Usage + +This template follows the [Keep a Changelog](https://keepachangelog.com/) format. When making changes: + +### Adding New Entries + +```markdown +## [Unreleased] + +### Added +- New feature description + +### Changed +- Existing feature update + +### Fixed +- Bug fix description + +### Security +- Security improvements +``` + +### Version Numbering + +- **MAJOR** - Breaking changes +- **MINOR** - New features (backward compatible) +- **PATCH** - Bug fixes (backward compatible) + +### Date Format + +Use ISO 8601 format: `YYYY-MM-DD` + +--- + +## Links + +- [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +- [Semantic Versioning](https://semver.org/spec/v2.0.0.html) +- [Conventional Commits](https://www.conventionalcommits.org/) + +--- + +Generated by [Zercle](https://github.com/zercle) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..847a72e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,390 @@ +# Contributing to Zercle Bun Template + +Thank you for your interest in contributing to the Zercle Bun Template! This document provides guidelines and instructions for contributing. + +--- + +## 📋 Table of Contents + +- [Code of Conduct](#-code-of-conduct) +- [Development Setup](#-development-setup) +- [Branch Naming Conventions](#-branch-naming-conventions) +- [Commit Message Format](#-commit-message-format) +- [Pull Request Process](#-pull-request-process) +- [Testing Requirements](#-testing-requirements) +- [Code Style Guidelines](#-code-style-guidelines) + +--- + +## 📜 Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). By participating, you are expected to uphold this code. + +### Our Standards + +- **Be Respectful** - Treat everyone with respect and kindness +- **Be Inclusive** - Welcome newcomers and different perspectives +- **Be Constructive** - Provide helpful feedback and constructive criticism +- **Be Professional** - Maintain professional communication + +### Unacceptable Behavior + +- Harassment or discrimination +- Personal attacks or insults +- Unwelcome sexual attention +- Publishing private information without consent + +--- + +## 💻 Development Setup + +### Prerequisites + +Ensure you have the following installed: + +- [Bun](https://bun.sh) ≥ 1.0.0 +- [Git](https://git-scm.com) ≥ 2.0 +- [Docker](https://www.docker.com) ≥ 20.10 (optional) + +### Initial Setup + +1. **Fork the repository** + + Click the "Fork" button on GitHub and clone your fork: + + ```bash + git clone https://github.com/YOUR-USERNAME/zercle-bun-template.git + cd zercle-bun-template + ``` + +2. **Add upstream remote** + + ```bash + git remote add upstream https://github.com/zercle/zercle-bun-template.git + ``` + +3. **Install dependencies** + + ```bash + bun install + ``` + +4. **Configure environment** + + ```bash + cp .env.example .env + ``` + +5. **Verify setup** + + ```bash + bun run dev + ``` + + Visit `http://localhost:3000/health` to confirm the server is running. + +### Keeping Your Fork Updated + +```bash +# Fetch upstream changes +git fetch upstream + +# Merge upstream main into your branch +git merge upstream/main + +# Push updates to your fork +git push origin main +``` + +--- + +## 🌿 Branch Naming Conventions + +We follow a structured branch naming convention: + +| Type | Pattern | Example | +|------|---------|---------| +| Feature | `feature/*` | `feature/user-authentication` | +| Bugfix | `bugfix/*` | `bugfix/fix-login-issue` | +| Hotfix | `hotfix/*` | `hotfix/security-patch` | +| Release | `release/*` | `release/v1.0.0` | +| Documentation | `docs/*` | `docs/update-readme` | +| Refactor | `refactor/*` | `refactor/api-routes` | + +### Best Practices + +- Use kebab-case for branch names +- Include issue number if applicable: `feature/123-user-auth` +- Keep branch names descriptive and concise +- Delete merged branches + +--- + +## 📝 Commit Message Format + +This project uses [Conventional Commits](https://www.conventionalcommits.org/) for automated changelog generation. + +### Format + +``` +[optional scope]: + +[optional body] + +[optional footer] +``` + +### Types + +| Type | Description | +|------|-------------| +| `feat` | New feature or functionality | +| `fix` | Bug fix | +| `docs` | Documentation changes only | +| `style` | Code style changes (formatting, etc.) | +| `refactor` | Code refactoring without behavior change | +| `perf` | Performance improvements | +| `test` | Adding or modifying tests | +| `chore` | Maintenance tasks, dependency updates | +| `build` | Build system changes | +| `ci` | CI/CD pipeline changes | + +### Examples + +```bash +# Simple fix +fix(health): resolve timeout issue in health check + +# Feature with scope +feat(api): add user authentication endpoints + +# Breaking change +feat!: drop support for Node.js < 18 + +# Detailed change +feat(middleware): + implement new logging middleware + - Log request duration + - Log response status + - Include correlation IDs +``` + +### Scopes + +Common scopes for this project: +- `api` - API routes +- `middleware` - Middleware changes +- `config` - Configuration changes +- `health` - Health check endpoints +- `deps` - Dependency updates +- `ci` - CI/CD pipeline + +--- + +## 🔄 Pull Request Process + +### Before Creating a PR + +1. **Update your branch** + + ```bash + git fetch upstream + git merge upstream/main + ``` + +2. **Run tests** + + ```bash + bun run test + ``` + +3. **Run linting** + + ```bash + bun run lint + ``` + +4. **Format code** + + ```bash + bun run format + ``` + +5. **Verify type checking** + + ```bash + bun run typecheck + ``` + +### Creating a PR + +1. **Push your branch** + + ```bash + git push origin feature/your-feature + ``` + +2. **Open PR on GitHub** + + Navigate to the original repository and click "New Pull Request" + +3. **Fill PR template** + + Provide: + - Clear title following conventional commits + - Detailed description of changes + - Link to related issues + - Screenshots for UI changes + - Test results + +### PR Requirements + +- [ ] All tests pass +- [ ] No linting errors +- [ ] Code formatted correctly +- [ ] TypeScript compilation succeeds +- [ ] Changes are documented +- [ ] At least one reviewer approval + +### PR Review Process + +1. **Reviewer assignment** - Maintainers review within 48 hours +2. **Automated checks** - CI/CD runs automatically +3. **Code review** - Reviewer provides feedback +4. **Revisions** - Author makes requested changes +5. **Approval** - Reviewer approves +6. **Merge** - Author or maintainer merges + +--- + +## ✅ Testing Requirements + +### Test Coverage + +All contributions must include appropriate tests: + +- **Unit tests** - For utility functions and middleware +- **Integration tests** - For API endpoints +- **E2E tests** - For critical user flows + +### Running Tests + +```bash +# Run all tests +bun run test + +# Run tests in watch mode +bun run test:watch + +# Run tests with coverage +bun run test:coverage +``` + +### Writing Tests + +Place tests in the `test/` directory: + +```typescript +// test/health.test.ts +import { describe, it, expect } from 'bun:test' +import { app } from '../src/index' + +describe('Health Check', () => { + it('should return healthy status', async () => { + const req = new Request('http://localhost/health') + const res = await app.fetch(req) + expect(res.status).toBe(200) + + const body = await res.json() + expect(body.status).toBe('ok') + }) +}) +``` + +### Test Coverage Targets + +| Type | Minimum Coverage | +|------|------------------| +| Unit tests | 80% | +| Integration tests | 70% | +| Critical paths | 100% | + +--- + +## 🎨 Code Style Guidelines + +### TypeScript + +- Enable `strict: true` in `tsconfig.json` +- Use interfaces for object shapes +- Use types for unions and primitives +- Prefer explicit return types for public functions + +### Code Organization + +``` +src/ +├── config/ # Configuration files +├── middleware/ # Express/Hono middleware +├── routes/ # Route handlers +├── types/ # TypeScript definitions +├── utils/ # Utility functions +└── index.ts # Entry point +``` + +### Naming Conventions + +| Element | Convention | Example | +|---------|------------|---------| +| Variables | camelCase | `userName` | +| Constants | SCREAMING_SNAKE_CASE | `MAX_CONNECTIONS` | +| Functions | camelCase | `getUserById` | +| Classes | PascalCase | `UserService` | +| Interfaces | PascalCase | `IUser` or `UserType` | +| Files | kebab-case | `user-service.ts` | + +### Formatting + +We use Prettier for code formatting: + +```bash +# Format all files +bun run format + +# Check formatting +bun run format:check +``` + +### Linting + +ESLint enforces code quality rules: + +```bash +# Check for issues +bun run lint + +# Auto-fix issues +bun run lint:fix +``` + +### Best Practices + +1. **Keep functions small** - Under 50 lines preferred +2. **Single responsibility** - One function, one purpose +3. **Avoid magic numbers** - Use named constants +4. **Error handling** - Always handle errors explicitly +5. **Comments** - Document complex logic, not obvious code +6. **Imports** - Use absolute imports with path aliases + +--- + +## 📚 Additional Resources + +- [ARCHITECTURE.md](ARCHITECTURE.md) - Technical architecture details +- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) - Detailed development guide +- [docs/API.md](docs/API.md) - API documentation +- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) - Deployment instructions + +--- + +Thank you for contributing! 🚀 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..436286e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# ============================================ +# Build Stage - Using oven/bun for compilation +# ============================================ +FROM oven/bun:1-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json bun.lockb ./ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the application +RUN bun run build + +# ============================================ +# Production Stage - Minimal image +# ============================================ +FROM oven/bun:1-alpine AS production + +WORKDIR /app + +# Create non-root user for security +RUN addgroup -g 1001 -S appgroup && \ + adduser -S appuser -u 1001 -G appgroup + +# Copy package files +COPY package.json bun.lockb ./ + +# Install production dependencies only +RUN bun install --frozen-lockfile --production + +# Copy built artifacts from builder +COPY --from=builder /app/dist ./dist + +# Change ownership to non-root user +RUN chown -R appuser:appgroup /app + +# Switch to non-root user +USER appuser + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 + +# Environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Start the application +CMD ["bun", "run", "start"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 895e169..0000000 --- a/Makefile +++ /dev/null @@ -1,236 +0,0 @@ -# ============================================================================= -# Zercle Bun Template - Makefile -# ============================================================================= -# A comprehensive Makefile for the Bun-based RESTful API template -# Using Hono framework with PostgreSQL and Drizzle ORM -# ============================================================================= - -# ----------------------------------------------------------------------------- -# Configuration Variables -# ----------------------------------------------------------------------------- -BUN := bun -DOCKER_COMPOSE := docker-compose -DOCKER_COMPOSE_TEST := docker-compose -f deployments/docker/docker-compose.test.yml -DOCKER_COMPOSE_PROD := docker-compose -f deployments/docker/docker-compose.yml - -# Colors for terminal output -GREEN := \033[0;32m -BLUE := \033[0;34m -YELLOW := \033[0;33m -RED := \033[0;31m -CYAN := \033[0;36m -RESET := \033[0m - -# ----------------------------------------------------------------------------- -# Development Targets -# ----------------------------------------------------------------------------- -.PHONY: install dev build start - -install: # Install dependencies using bun install - @echo "$(GREEN)Installing dependencies...$(RESET)" - @$(BUN) install - -dev: # Run development server with hot reload - @echo "$(GREEN)Starting development server...$(RESET)" - @$(BUN) run dev - -build: # Build the project for production - @echo "$(GREEN)Building project...$(RESET)" - @$(BUN) run build - -start: # Start production server - @echo "$(GREEN)Starting production server...$(RESET)" - @$(BUN) run start - -# ----------------------------------------------------------------------------- -# Testing Targets -# ----------------------------------------------------------------------------- -.PHONY: test test:unit test:integration test:coverage test:watch - -test: # Run all tests - @echo "$(GREEN)Running all tests...$(RESET)" - @$(BUN) run test - -test:unit: # Run unit tests only - @echo "$(GREEN)Running unit tests...$(RESET)" - @$(BUN) run test tests/unit/ - -test:integration: # Run integration tests only - @echo "$(GREEN)Running integration tests...$(RESET)" - @$(BUN) run test:integration - -test:coverage: # Run tests with coverage report - @echo "$(GREEN)Running tests with coverage...$(RESET)" - @$(BUN) run test:coverage - -test:watch: # Run tests in watch mode - @echo "$(GREEN)Running tests in watch mode...$(RESET)" - @$(BUN) run test --watch - -# ----------------------------------------------------------------------------- -# Code Quality Targets -# ----------------------------------------------------------------------------- -.PHONY: lint lint:fix format format:check check - -lint: # Run linter to check code quality - @echo "$(GREEN)Running linter...$(RESET)" - @$(BUN) run lint - -lint:fix # Fix linting issues automatically - @echo "$(GREEN)Fixing linting issues...$(RESET)" - @$(BUN) run lint:fix - -format: # Format code using Prettier - @echo "$(GREEN)Formatting code...$(RESET)" - @$(BUN) run format - -format:check: # Check code formatting without modifying - @echo "$(GREEN)Checking code formatting...$(RESET)" - @$(BUN) run format:check - -check: # Run all checks (lint + format:check) - @echo "$(GREEN)Running all code quality checks...$(RESET)" - @$(BUN) run lint - @$(BUN) run format:check - -# ----------------------------------------------------------------------------- -# Database Targets -# ----------------------------------------------------------------------------- -.PHONY: db:generate db:push db:migrate db:studio - -db:generate: # Generate database migrations from schema - @echo "$(GREEN)Generating database migrations...$(RESET)" - @$(BUN) run db:generate - -db:push: # Push schema changes to database (development) - @echo "$(GREEN)Pushing schema to database...$(RESET)" - @$(BUN) run db:push - -db:migrate: # Run pending database migrations - @echo "$(GREEN)Running database migrations...$(RESET)" - @$(BUN) run db:migrate - -db:studio: # Open Drizzle Studio for database management - @echo "$(GREEN)Opening Drizzle Studio...$(RESET)" - @$(BUN) run db:studio - -# ----------------------------------------------------------------------------- -# Docker Targets -# ----------------------------------------------------------------------------- -.PHONY: docker:build docker:up docker:down docker:logs docker:clean - -docker:build: # Build Docker image for production - @echo "$(GREEN)Building Docker image...$(RESET)" - @$(DOCKER_COMPOSE_PROD) build - -docker:up: # Start Docker containers in detached mode - @echo "$(GREEN)Starting Docker containers...$(RESET)" - @$(DOCKER_COMPOSE_PROD) up -d - -docker:down: # Stop Docker containers - @echo "$(GREEN)Stopping Docker containers...$(RESET)" - @$(DOCKER_COMPOSE_PROD) down - -docker:logs: # View Docker container logs with follow - @echo "$(GREEN)Viewing Docker logs (Ctrl+C to exit)...$(RESET)" - @$(DOCKER_COMPOSE_PROD) logs -f - -docker:clean: # Remove Docker containers, networks, and volumes - @echo "$(RED)Cleaning up Docker resources...$(RESET)" - @$(DOCKER_COMPOSE_PROD) down -v - -# ----------------------------------------------------------------------------- -# Docker Test Targets -# ----------------------------------------------------------------------------- -.PHONY: docker:test:up docker:test:down docker:test:logs - -docker:test:up: # Start test database container - @echo "$(GREEN)Starting test database...$(RESET)" - @$(DOCKER_COMPOSE_TEST) up -d - -docker:test:down: # Stop test database container - @echo "$(GREEN)Stopping test database...$(RESET)" - @$(DOCKER_COMPOSE_TEST) down -v - -docker:test:logs: # View test container logs - @echo "$(GREEN)Viewing test container logs...$(RESET)" - @$(DOCKER_COMPOSE_TEST) logs -f - -# ----------------------------------------------------------------------------- -# Utility Targets -# ----------------------------------------------------------------------------- -.PHONY: clean clean:build fresh help - -clean: # Clean all build artifacts and node_modules - @echo "$(RED)Cleaning all build artifacts...$(RESET)" - @rm -rf dist - @rm -rf node_modules - @rm -rf .bun - @echo "$(GREEN)Clean complete!$(RESET)" - -clean:build: # Clean build artifacts only (keep node_modules) - @echo "$(YELLOW)Cleaning build artifacts...$(RESET)" - @rm -rf dist - @rm -rf .bun - @echo "$(GREEN)Build artifacts cleaned!$(RESET)" - -fresh: # Clean and reinstall dependencies - @echo "$(CYAN)Performing fresh install...$(RESET)" - @$(MAKE) clean - @$(MAKE) install - -help: # Display this help message - @echo "" - @echo "$(CYAN)========================================$(RESET)" - @echo "$(CYAN) Zercle Bun Template - Available Commands$(RESET)" - @echo "$(CYAN)========================================$(RESET)" - @echo "" - @echo "$(BLUE)Development:$(RESET)" - @echo " $(YELLOW)make install$(RESET) Install dependencies" - @echo " $(YELLOW)make dev$(RESET) Start development server" - @echo " $(YELLOW)make build$(RESET) Build for production" - @echo " $(YELLOW)make start$(RESET) Start production server" - @echo "" - @echo "$(BLUE)Testing:$(RESET)" - @echo " $(YELLOW)make test$(RESET) Run all tests" - @echo " $(YELLOW)make test:unit$(RESET) Run unit tests" - @echo " $(YELLOW)make test:integration$(RESET) Run integration tests" - @echo " $(YELLOW)make test:coverage$(RESET) Run tests with coverage" - @echo " $(YELLOW)make test:watch$(RESET) Run tests in watch mode" - @echo "" - @echo "$(BLUE)Code Quality:$(RESET)" - @echo " $(YELLOW)make lint$(RESET) Run linter" - @echo " $(YELLOW)make lint:fix$(RESET) Fix linting issues" - @echo " $(YELLOW)make format$(RESET) Format code" - @echo " $(YELLOW)make format:check$(RESET) Check formatting" - @echo " $(YELLOW)make check$(RESET) Run all checks" - @echo "" - @echo "$(BLUE)Database:$(RESET)" - @echo " $(YELLOW)make db:generate$(RESET) Generate migrations" - @echo " $(YELLOW)make db:push$(RESET) Push schema to DB" - @echo " $(YELLOW)make db:migrate$(RESET) Run migrations" - @echo " $(YELLOW)make db:studio$(RESET) Open Drizzle Studio" - @echo "" - @echo "$(BLUE)Docker (Production):$(RESET)" - @echo " $(YELLOW)make docker:build$(RESET) Build Docker image" - @echo " $(YELLOW)make docker:up$(RESET) Start containers" - @echo " $(YELLOW)make docker:down$(RESET) Stop containers" - @echo " $(YELLOW)make docker:logs$(RESET) View logs" - @echo " $(YELLOW)make docker:clean$(RESET) Remove containers & volumes" - @echo "" - @echo "$(BLUE)Docker (Testing):$(RESET)" - @echo " $(YELLOW)make docker:test:up$(RESET) Start test DB" - @echo " $(YELLOW)make docker:test:down$(RESET) Stop test DB" - @echo " $(YELLOW)make docker:test:logs$(RESET) View test logs" - @echo "" - @echo "$(BLUE)Utilities:$(RESET)" - @echo " $(YELLOW)make clean$(RESET) Clean all artifacts" - @echo " $(YELLOW)make clean:build$(RESET) Clean build only" - @echo " $(YELLOW)make fresh$(RESET) Clean + reinstall" - @echo " $(YELLOW)make help$(RESET) Show this help" - @echo "" - @echo "$(CYAN)========================================$(RESET)" - @echo "" - -.PHONY: default -default: help diff --git a/README.md b/README.md index de1fcdd..619e46b 100644 --- a/README.md +++ b/README.md @@ -1,322 +1,202 @@ -# Zercle Bun Template +# 🐰 Zercle Bun Template
-![Bun](https://img.shields.io/badge/Bun-1.0.0-black?style=for-the-badge&logo=bun) -![Hono](https://img.shields.io/badge/Hono-4.6.0-red?style=for-the-badge) -![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue?style=for-the-badge&logo=typescript) -![Drizzle ORM](https://img.shields.io/badge/Drizzle%20ORM-0.36.0-purple?style=for-the-badge) -![Docker](https://img.shields.io/badge/Docker-Ready-blue?style=for-the-badge&logo=docker) +[![Bun](https://img.shields.io/badge/Bun-1.0.0-black?logo=bun)](https://bun.sh) +[![Hono](https://img.shields.io/badge/Hono-4.0-yellow?logo=hono)](https://hono.dev) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue?logo=typescript)](https://www.typescriptlang.org/) +[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE.md) +[![Docker](https://img.shields.io/badge/Docker-Ready-blue?logo=docker)](https://www.docker.com/) -A production-ready RESTful API template built with **Bun runtime** and **Hono framework**. This template implements domain-driven design (DDD) architecture with clean separation of concerns, making it ideal for building scalable, maintainable backend services. - -[Features](#features) • [Tech Stack](#tech-stack) • [Project Structure](#project-structure) • [Quick Start](#quick-start) • [Documentation](#documentation) +A production-ready Hono.js + Bun template for building high-performance APIs.
--- -## Features - -- 🚀 **High Performance**: Built on Bun, the fastest JavaScript runtime -- 🎯 **Domain-Driven Design**: Clean architecture with separated domains, infrastructure, and utilities -- 🔐 **Secure Authentication**: JWT-based auth with Argon2id password hashing -- 📊 **Database Integration**: Drizzle ORM with PostgreSQL and type-safe migrations -- 🛡️ **Security Middleware**: CORS, rate limiting, request ID tracking, and JWT verification -- 📝 **TypeScript**: Full type safety with strict mode enabled -- 🐳 **Docker Ready**: Production-ready Docker configuration with docker-compose -- 📊 **Structured Logging**: Pino-based logging with configurable formats -- ✅ **API Response Format**: Consistent JSend-style response structure -- 🔧 **Configuration Management**: YAML-based config with environment variable overrides - -## Tech Stack - -| Category | Technology | -| -------------- | ----------------------------------------------------------------------------------------------- | -| Runtime | [Bun](https://bun.sh/) v1.0.0+ | -| Framework | [Hono](https://hono.dev/) v4.6.0 | -| Language | [TypeScript](https://www.typescriptlang.org/) v5.7 | -| Database | [PostgreSQL](https://www.postgresql.org/) with [Drizzle ORM](https://orm.drizzle.team/) v0.36.0 | -| Authentication | [JWT](https://jwt.io/) + [Argon2id](https://github.com/ranisalt/node-argon2) | -| Validation | [Zod](https://zod.dev/) v3.24 | -| Logging | [Pino](https://getpino.io/) v9.6 | -| Configuration | [js-yaml](https://github.com/nodeca/js-yaml) v4.1 | -| Docker | [Docker](https://www.docker.com/) + [docker-compose](https://docs.docker.com/compose/) | - -## Project Structure +## 📋 Table of Contents -``` -zercle-bun-template/ -├── .env.example # Environment variables template -├── .gitignore # Git ignore rules -├── drizzle.config.ts # Drizzle ORM configuration -├── package.json # Project dependencies and scripts -├── tsconfig.json # TypeScript configuration -├── LICENSE.md # License file -│ -├── configs/ # Configuration files by environment -│ ├── dev.yaml # Development configuration -│ ├── local.yaml # Local development configuration -│ ├── prod.yaml # Production configuration -│ └── uat.yaml # User acceptance testing configuration -│ -├── deployments/ # Deployment configurations -│ └── docker/ -│ ├── Dockerfile # Multi-stage Docker build -│ └── docker-compose.yml # Docker Compose for local development -│ -├── drizzle/ # Database migrations -│ └── migrations/ # Generated migration files -│ └── *_initial_schema.sql -│ -├── scripts/ # Utility scripts -│ -└── src/ # Source code - ├── main.ts # Application entry point - ├── app.ts # App initialization and route setup - │ - ├── domain/ # Business logic (DDD) - │ ├── task/ # Task domain - │ │ ├── entity/ # Domain entities (Task model) - │ │ ├── handler/ # HTTP handlers/controllers - │ │ ├── repository/ # Data access layer - │ │ ├── request/ # Request validation schemas - │ │ ├── response/ # Response types - │ │ └── usecase/ # Business logic use cases - │ │ - │ └── user/ # User domain - │ ├── entity/ # Domain entities (User model) - │ ├── handler/ # HTTP handlers/controllers - │ ├── repository/ # Data access layer - │ ├── request/ # Request validation schemas - │ ├── response/ # Response types - │ └── usecase/ # Business logic use cases - │ - ├── infrastructure/ # Infrastructure layer - │ ├── config/ # Configuration loading and validation - │ ├── db/ # Database connection and setup - │ ├── logger/ # Logging service - │ ├── middleware/ # HTTP middleware - │ │ ├── auth.ts # JWT authentication - │ │ ├── cors.ts # CORS handling - │ │ ├── logger.ts # Request/response logging - │ │ ├── rate-limit.ts # Rate limiting - │ │ └── request-id.ts # Request ID generation - │ └── password/ # Password hashing (Argon2id) - │ - └── utils/ # Utility functions - └── response.ts # JSend-formatted response helpers -``` +- [✨ Features](#-features) +- [🚀 Quick Start](#-quick-start) +- [📦 Available Scripts](#-available-scripts) +- [🏗️ Project Structure](#️-project-structure) +- [🔧 Environment Variables](#-environment-variables) +- [🌐 API Endpoints](#-api-endpoints) +- [🔄 Development Workflow](#-development-workflow) +- [🐳 Docker Usage](#-docker-usage) +- [📦 Deployment](#-deployment) +- [🤝 Contributing](#-contributing) +- [📄 License](#-license) -### Architecture Overview +--- -This template follows **Domain-Driven Design (DDD)** principles with a clear separation of layers: +## ✨ Features -``` -┌─────────────────────────────────────────────────────────────┐ -│ Presentation Layer │ -│ (Handlers / Controllers) │ -├─────────────────────────────────────────────────────────────┤ -│ Application Layer │ -│ (Use Cases) │ -├─────────────────────────────────────────────────────────────┤ -│ Domain Layer │ -│ (Entities / Business Logic) │ -├─────────────────────────────────────────────────────────────┤ -│ Infrastructure Layer │ -│ (DB, Auth, Config, Middleware) │ -└─────────────────────────────────────────────────────────────┘ -``` +This template provides a solid foundation for building high-performance APIs with Hono.js and Bun: -## Prerequisites +- **TypeScript** - Full type safety with strict configuration +- **Hono.js** - Lightweight, fast web framework +- **Bun Runtime** - High-performance JavaScript runtime +- **Modular Architecture** - Clean separation of routes, middleware, and utilities +- **Built-in Middleware** - Error handling, logging, and not-found handlers +- **Testing Setup** - Ready-to-use testing with Bun test runner +- **Code Quality** - ESLint and Prettier integration +- **Docker Support** - Production-ready Docker configuration +- **CI/CD Pipeline** - GitHub Actions workflow included +- **Conventional Commits** - Standardized commit message format -Before using this template, ensure you have the following installed: +--- -- **Bun** v1.0.0 or higher - [Install](https://bun.sh/docs/installation) -- **Node.js** v18 or higher (for some tooling) -- **PostgreSQL** v15+ (or use Docker) -- **Git** for version control +## 🚀 Quick Start -## Quick Start +### Prerequisites -### 1. Clone and Install Dependencies +- [Bun](https://bun.sh) ≥ 1.0.0 +- [Node.js](https://nodejs.org) ≥ 18.0.0 (optional, for npm compatibility) +- [Docker](https://www.docker.com) ≥ 20.10 (for containerized development) -```bash -# Clone the repository -git clone -cd zercle-bun-template - -# Install dependencies -bun install -``` +### Installation -### 2. Configure Environment +1. **Clone the repository** ```bash -# Copy environment template -cp .env.example .env - -# Edit environment variables -nano .env +git clone https://github.com/zercle/zercle-bun-template.git +cd zercle-bun-template ``` -### 3. Set Up Database - -**Option A: Using Docker Compose (Recommended)** +2. **Install dependencies** ```bash -# Start PostgreSQL container -docker-compose -f deployments/docker/docker-compose.yml up -d postgres - -# Run database migrations -bun run db:migrate +bun install ``` -**Option B: Local PostgreSQL** - -Ensure PostgreSQL is running locally, then: +3. **Configure environment variables** ```bash -# Run migrations -bun run db:migrate +cp .env.example .env ``` -### 4. Start Development Server +4. **Start development server** ```bash -# Start with hot reload bun run dev ``` -The server will start at `http://localhost:3000`. - -## Configuration - -### Environment Variables - -Configure your application using environment variables or `.env` file: - -```env -# Server Configuration -SERVER_ENV=local -SERVER_PORT=3000 -SERVER_HOST=0.0.0.0 - -# Database Configuration -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=postgres -DB_NAME=postgres -DB_DRIVER=postgres - -# JWT Configuration -JWT_SECRET=your-secret-key-change-in-production -JWT_EXPIRATION=3600 - -# Logging Configuration -LOG_LEVEL=info -LOG_FORMAT=json - -# CORS Configuration -# CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 - -# Rate Limiting Configuration -RATE_LIMIT_REQUESTS=100 -RATE_LIMIT_WINDOW=60 - -# Argon2id Configuration -ARGON2ID_MEMORY=19456 -ARGON2ID_ITERATIONS=2 -ARGON2ID_PARALLELISM=1 -``` - -### YAML Configuration Files - -The application uses YAML configuration files per environment: +5. **Verify the installation** -| File | Environment | Description | -| -------------------- | ----------- | -------------------------- | -| `configs/local.yaml` | Local | Local development settings | -| `configs/dev.yaml` | Development | Development environment | -| `configs/uat.yaml` | UAT | User acceptance testing | -| `configs/prod.yaml` | Production | Production settings | - -To change the active environment, set `SERVER_ENV`: +Visit `http://localhost:3000` in your browser or run: ```bash -SERVER_ENV=dev bun run dev +curl http://localhost:3000/health ``` -## Database Setup - -### Database Migrations - -```bash -# Generate a new migration (after schema changes) -bun run db:generate - -# Push schema changes (development only) -bun run db:push +You should see a healthy response: -# Run all migrations -bun run db:migrate - -# Open Drizzle Studio (database GUI) -bun run db:studio +```json +{ + "status": "ok", + "timestamp": "2024-01-15T10:00:00.000Z" +} ``` -### Database Schema +--- -The database schema is defined in [`src/infrastructure/db/drizzle.ts`](src/infrastructure/db/drizzle.ts). After making changes: +## 📦 Available Scripts -1. Update the schema file -2. Run `bun run db:generate` to create a new migration -3. Review the generated SQL in `drizzle/migrations/` -4. Run `bun run db:migrate` to apply +| Command | Description | +|---------|-------------| +| `bun run dev` | Start development server with hot reload | +| `bun run build` | Compile TypeScript to JavaScript | +| `bun run start` | Start production server | +| `bun run test` | Run test suite | +| `bun run lint` | Run ESLint to check code quality | +| `bun run format` | Format code with Prettier | +| `bun run typecheck` | Run TypeScript type checking | +| `docker-compose up` | Start services with Docker Compose | +| `docker-compose up -d` | Start services in detached mode | -## Running the Application +--- -### Development Mode +## 🏗️ Project Structure -```bash -# Start with hot reload -bun run dev +``` +zercle-bun-template/ +├── .github/ +│ └── workflows/ # CI/CD pipelines +├── docs/ # Documentation +│ ├── API.md # API documentation +│ ├── DEPLOYMENT.md # Deployment guide +│ └── DEVELOPMENT.md # Development guide +├── src/ +│ ├── config/ +│ │ └── env.ts # Environment configuration +│ ├── middleware/ +│ │ ├── error-handler.ts # Error handling middleware +│ │ ├── logger.ts # Request logging middleware +│ │ └── not-found.ts # 404 handler +│ ├── routes/ +│ │ ├── api.ts # API route definitions +│ │ ├── health.ts # Health check endpoints +│ │ └── index.ts # Route aggregation +│ ├── types/ +│ │ └── index.ts # TypeScript type definitions +│ ├── utils/ +│ │ └── logger.ts # Logging utilities +│ └── index.ts # Application entry point +├── test/ +│ ├── setup.ts # Test setup configuration +│ ├── health.test.ts # Health check tests +│ └── middleware/ +│ └── error-handler.test.ts # Middleware tests +├── .env.example # Environment variables template +├── .prettierrc # Prettier configuration +├── eslint.config.js # ESLint configuration +├── Dockerfile # Docker image definition +├── docker-compose.yml # Docker Compose configuration +├── tsconfig.json # TypeScript configuration +└── package.json # Project dependencies ``` -### Production Mode +--- -```bash -# Build the application -bun run build +## 🔧 Environment Variables -# Start production server -bun run start -``` +Create a `.env` file based on the `.env.example` template: -### Running Tests +```env +# Server Configuration +PORT=3000 +NODE_ENV=development -```bash -# Run all tests -bun run test +# Application Settings +APP_NAME=zercle-bun-template +APP_VERSION=0.1.0 + +# Logging +LOG_LEVEL=debug -# Run tests with coverage -bun run test:coverage +# CORS Configuration +CORS_ORIGIN=http://localhost:3000 +CORS_METHODS=GET,POST,PUT,DELETE,PATCH +CORS_HEADERS=Content-Type,Authorization ``` -### Linting and Formatting +### Environment Variables Reference -```bash -# Check code style -bun run lint -bun run format:check +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `PORT` | No | `3000` | Port number for the server | +| `NODE_ENV` | No | `development` | Environment mode | +| `APP_NAME` | No | Template name | Application identifier | +| `APP_VERSION` | No | `0.1.0` | Application version | +| `LOG_LEVEL` | No | `debug` | Logging verbosity | +| `CORS_ORIGIN` | No | `*` | Allowed CORS origin | +| `CORS_METHODS` | No | All methods | Allowed HTTP methods | +| `CORS_HEADERS` | No | Standard headers | Allowed headers | -# Auto-fix issues -bun run lint:fix -bun run format -``` +--- -## API Documentation +## 🌐 API Endpoints ### Base URL @@ -326,346 +206,166 @@ http://localhost:3000 ### Health Check Endpoints -| Method | Endpoint | Description | -| ------ | ------------ | -------------------------------------- | -| GET | `/health` | Application health check | -| GET | `/readiness` | Readiness probe (checks DB connection) | - -### Authentication Endpoints - -| Method | Endpoint | Auth | Description | -| ------ | ----------------------- | ---- | ----------------------- | -| POST | `/api/v1/auth/register` | ❌ | Register a new user | -| POST | `/api/v1/auth/login` | ❌ | Login and get JWT token | - -### User Endpoints - -| Method | Endpoint | Auth | Description | -| ------ | ----------------------- | ---- | -------------------------- | -| GET | `/api/v1/users/profile` | ✅ | Get current user profile | -| PUT | `/api/v1/users/profile` | ✅ | Update user profile | -| DELETE | `/api/v1/users/profile` | ✅ | Delete user account | -| GET | `/api/v1/users` | ✅ | List all users (paginated) | - -### Task Endpoints - -| Method | Endpoint | Auth | Description | -| ------ | ------------------- | ---- | -------------------------- | -| POST | `/api/v1/tasks` | ✅ | Create a new task | -| GET | `/api/v1/tasks` | ✅ | List all tasks (paginated) | -| GET | `/api/v1/tasks/:id` | ✅ | Get task by ID | -| PUT | `/api/v1/tasks/:id` | ✅ | Update a task | -| DELETE | `/api/v1/tasks/:id` | ✅ | Delete a task | - -### Request/Response Examples - -**Register User** +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/health` | Basic health check | +| `GET` | `/health/ready` | Readiness probe | +| `GET` | `/health/live` | Liveness probe | -```http -POST /api/v1/auth/register -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "securePassword123", - "fullName": "John Doe", - "phone": "+1234567890" -} -``` - -**Response (201 Created)** - -```json -{ - "status": "success", - "data": { - "id": "uuid", - "email": "user@example.com", - "fullName": "John Doe", - "isActive": true, - "createdAt": "2024-01-01T00:00:00.000Z" - } -} -``` - -**Login** - -```http -POST /api/v1/auth/login -Content-Type: application/json - -{ - "email": "user@example.com", - "password": "securePassword123" -} -``` - -**Response (200 OK)** +#### Health Check Response ```json { - "status": "success", - "data": { - "token": "eyJhbGciOiJIUzI1NiIs...", - "expiresIn": 3600 - } + "status": "ok", + "timestamp": "2024-01-15T10:00:00.000Z", + "version": "0.1.0" } ``` -### Error Response Format +### API Routes -All errors follow the JSend specification: +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/api` | API root with available routes | +| `GET` | `/api/resource` | Get resource (placeholder) | +| `POST` | `/api/resource` | Create resource (placeholder) | -```json -{ - "status": "fail", - "message": "User not found", - "data": { - "field": ["error message"] - } -} -``` - -## Docker Deployment - -### Building the Image +#### Example Request ```bash -# Build the Docker image -docker build -f deployments/docker/Dockerfile -t zercle-bun-app . +curl -X GET http://localhost:3000/health ``` -### Running with Docker Compose - -```bash -# Start all services (app + database) -docker-compose -f deployments/docker/docker-compose.yml up -d - -# View logs -docker-compose -f deployments/docker/docker-compose.yml logs -f - -# Stop services -docker-compose -f deployments/docker/docker-compose.yml down -``` - -### Production Deployment - -```bash -# Set production environment variables -export JWT_SECRET="your-production-secret" -export SERVER_ENV=prod - -# Build and run -docker-compose -f deployments/docker/docker-compose.yml up -d --build -``` +--- -### Health Checks +## 🔄 Development Workflow -The Docker configuration includes health checks for both the application and database: +### Git Workflow -- **Application Health**: `curl -f http://localhost:3000/health` -- **Database Health**: `pg_isready -U postgres` +1. **Create a branch** from `main` +2. **Make changes** and commit following conventions +3. **Push** your branch +4. **Create a Pull Request** for review +5. **Merge** after approval -## Development Guidelines +### Branch Naming -### Adding a New Domain +- `feature/*` - New features +- `bugfix/*` - Bug fixes +- `hotfix/*` - Urgent production fixes +- `release/*` - Release preparation +- `docs/*` - Documentation changes -To add a new domain (e.g., `order`): +### Commit Message Format -1. **Create the domain structure:** +This project uses [Conventional Commits](https://www.conventionalcommits.org/): -```bash -mkdir -p src/domain/order/{entity,handler,repository,request,response,usecase} ``` +[optional scope]: -2. **Define the entity:** - -```typescript -// src/domain/order/entity/order.ts -export interface Order { - id: string; - userId: string; - status: OrderStatus; - totalAmount: number; - createdAt: Date; - updatedAt: Date; -} +[optional body] -export interface CreateOrder { - userId: string; - items: OrderItem[]; -} +[optional footer(s)] ``` -3. **Create the repository:** - -```typescript -// src/domain/order/repository/order.ts -import { DrizzleDatabase } from "../../../infrastructure/db/drizzle.js"; -import { Logger } from "../../../infrastructure/logger/logger.js"; -import { Order } from "../entity/order.js"; +**Types:** +- `feat` - New feature +- `fix` - Bug fix +- `docs` - Documentation only +- `style` - Code style changes +- `refactor` - Code refactoring +- `perf` - Performance improvements +- `test` - Adding tests +- `chore` - Maintenance tasks -export class OrderRepository { - constructor( - private db: DrizzleDatabase, - private logger: Logger, - ) {} +**Examples:** - async findById(id: string): Promise { - // Implementation - } - - async create(order: Order): Promise { - // Implementation - } -} ``` +feat(api): add user authentication endpoint -4. **Implement use cases:** +fix(middleware): resolve logging issue in production -```typescript -// src/domain/order/usecase/order.ts -export interface IOrderUseCase { - createOrder(data: CreateOrder): Promise; - getOrder(id: string): Promise; -} - -export class OrderUseCase implements IOrderUseCase { - // Implementation -} +docs(readme): update installation instructions ``` -5. **Create request validation:** - -```typescript -// src/domain/order/request/order.ts -import { z } from "zod"; - -export const createOrderSchema = z.object({ - userId: z.string().uuid(), - items: z.array( - z.object({ - productId: z.string().uuid(), - quantity: z.number().positive(), - }), - ), -}); -``` +--- -6. **Create HTTP handler:** +## 🐳 Docker Usage -```typescript -// src/domain/order/handler/order.ts -import { Context } from "hono"; -import { IOrderUseCase } from "../usecase/order.js"; -import { success, created } from "../../../utils/response.js"; +### Quick Start with Docker -export class OrderHandler { - constructor(private useCase: IOrderUseCase) {} +```bash +# Build and start the container +docker-compose up -d - async createOrder(c: Context) { - const body = await c.req.json(); - const validatedData = createOrderSchema.parse(body); - const order = await this.useCase.createOrder(validatedData); - return created(c, order); - } -} -``` +# View logs +docker-compose logs -f -7. **Register routes in `app.ts`:** +# Stop services +docker-compose down +``` -```typescript -// In setupDependencies() -const orderRepo = new OrderRepository(this.db, this.logger); -const orderUseCase = new OrderUseCase(orderRepo, this.logger); -const orderHandler = new OrderHandler(orderUseCase); +### Manual Docker Commands -// In registerOrderRoutes() -private registerOrderRoutes(handler: OrderHandler) { - const protectedRoutes = this.hono.basePath('/api/v1'); - protectedRoutes.use('/orders*', createAuthMiddleware(this.config.jwt)); +```bash +# Build the image +docker build -t zercle-bun-template . - protectedRoutes.post('/orders', (c) => handler.createOrder(c)); - protectedRoutes.get('/orders/:id', (c) => handler.getOrder(c)); -} +# Run the container +docker run -p 3000:3000 zercle-bun-template ``` -### Code Style - -- Use **ES modules** (`import`/`export`) -- Follow **strict TypeScript** mode -- Use **Zod** for input validation -- Return **JSend-formatted** responses -- Use **async/await** for all async operations -- Implement **proper error handling** with typed errors +### Docker Compose Services -### Naming Conventions +The `docker-compose.yml` includes: +- **app** - Main application service +- **Configuration** - Volume mounts for environment files +- **Port mapping** - 3000:3000 -| Component | Convention | Example | -| ------------------- | ---------------- | ----------------- | -| Files | kebab-case | `user-handler.ts` | -| Classes | PascalCase | `UserRepository` | -| Interfaces | PascalCase | `IUserService` | -| Variables/Functions | camelCase | `getUserById` | -| Constants | UPPER_SNAKE_CASE | `MAX_REQUESTS` | -| Database Tables | snake_case | `user_accounts` | - -## Testing +--- -### Writing Tests +## 📦 Deployment -Create test files with `.test.ts` extension: +### Docker Deployment -```typescript -// src/domain/user/usecase/user.test.ts -import { describe, it, expect } from "bun:test"; +```bash +# Build the production image +docker build -t zercle-bun-template:prod . -describe("User UseCase", () => { - it("should register a new user", async () => { - // Test implementation - }); -}); +# Run in production mode +docker run -d -p 3000:3000 \ + --env-file .env \ + --name zercle-api \ + zercle-bun-template:prod ``` -### Running Tests +### Environment-Specific Deployments -```bash -# Run all tests -bun test +See [DEPLOYMENT.md](docs/DEPLOYMENT.md) for detailed deployment instructions including: -# Run with coverage report -bun run test:coverage +- Docker deployment +- Environment configuration +- Health checks +- Monitoring and logging +- SSL/TLS configuration -# Run specific test file -bun test src/domain/user/usecase/user.test.ts -``` +--- -## Contributing +## 🤝 Contributing -1. **Fork the repository** -2. **Create a feature branch** (`git checkout -b feature/amazing-feature`) -3. **Commit changes** (`git commit -m 'Add amazing feature'`) -4. **Push to branch** (`git push origin feature/amazing-feature`) -5. **Open a Pull Request** +Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on: -### Commit Message Format +- Code of Conduct +- Development setup +- Branch naming conventions +- Commit message format +- Pull request process +- Testing requirements +- Code style guidelines -``` -type(scope): description - -Types: -- feat: New feature -- fix: Bug fix -- docs: Documentation changes -- style: Code style changes -- refactor: Code refactoring -- test: Test additions -- chore: Maintenance - -Example: feat(user): add password reset functionality -``` +--- -## License +## 📄 License This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. @@ -673,6 +373,6 @@ This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md
-Built with ❤️ using [Bun](https://bun.sh/) and [Hono](https://hono.dev/) +Made with 🐰 by [Zercle](https://github.com/zercle)
diff --git a/bun.lock b/bun.lock index 9b7bd46..77b2c3e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,172 +5,79 @@ "": { "name": "zercle-bun-template", "dependencies": { - "@hono/node-server": "^1.19.7", - "@hono/zod-validator": "^0.7.6", - "argon2": "^0.44.0", - "dotenv": "^17.2.3", - "drizzle-orm": "^0.45.1", - "hono": "^4.11.3", - "js-yaml": "^4.1.1", - "jsonwebtoken": "^9.0.3", - "pg": "^8.16.3", - "pino": "^10.1.0", - "postgres": "^3.4.7", - "uuid": "^13.0.0", - "zod": "^4.3.4", + "@hono/zod-validator": "^0.4.0", + "dotenv": "^16.4.0", + "hono": "^4.0.0", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "zod": "^3.22.0", }, "devDependencies": { - "@types/js-yaml": "^4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^25.0.3", - "@types/pg": "^8.16.0", - "@types/uuid": "^11.0.0", - "@typescript-eslint/eslint-plugin": "^8.51.0", - "@typescript-eslint/parser": "^8.51.0", - "bun-types": "^1.3.5", - "drizzle-kit": "^0.31.8", - "esbuild": "^0.27.2", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.7.4", - "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0", + "@types/bun": "^1.0.0", + "@types/node": "^20.11.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0", + "bun-types": "^1.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.0", + "typescript": "^5.3.0", }, }, }, - "overrides": { - "esbuild": ">=0.25.0", - }, + "trustedDependencies": [ + "@typescript-eslint/parser", + "@typescript-eslint/eslint-plugin", + "eslint", + ], "packages": { - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - - "@epic-web/invariant": ["@epic-web/invariant@1.0.0", "", {}, "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA=="], - - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], - "@hono/node-server": ["@hono/node-server@1.19.7", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw=="], - - "@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@phc/format": ["@phc/format@1.0.0", "", {}, "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ=="], - - "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], - "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], - "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], - "@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="], + "@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@7.18.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/type-utils": "7.18.0", "@typescript-eslint/utils": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.56.0" } }, "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@7.18.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.51.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.51.0", "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0" } }, "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@7.18.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "7.18.0", "@typescript-eslint/utils": "7.18.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.51.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@7.18.0", "", {}, "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^1.3.0" } }, "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@7.18.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", "@typescript-eslint/typescript-estree": "7.18.0" }, "peerDependencies": { "eslint": "^8.56.0" } }, "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.51.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.51.0", "@typescript-eslint/tsconfig-utils": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@7.18.0", "", { "dependencies": { "@typescript-eslint/types": "7.18.0", "eslint-visitor-keys": "^3.4.3" } }, "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -178,23 +85,27 @@ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "argon2": ["argon2@0.44.0", "", { "dependencies": { "@phc/format": "^1.0.0", "cross-env": "^10.0.0", "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" } }, "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], @@ -204,39 +115,37 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], - "cross-env": ["cross-env@10.1.0", "", { "dependencies": { "@epic-web/invariant": "^1.0.0", "cross-spawn": "^7.0.6" }, "bin": { "cross-env": "dist/bin/cross-env.js", "cross-env-shell": "dist/bin/cross-env-shell.js" } }, "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - - "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], - - "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], - "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], + "eslint-config-prettier": ["eslint-config-prettier@9.1.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ=="], - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], @@ -246,44 +155,76 @@ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], @@ -292,46 +233,30 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], - - "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], - - "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], - - "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], - - "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], - - "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], - - "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], - - "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - - "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], - "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -342,115 +267,107 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "pg": ["pg@8.16.3", "", { "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", "pg-protocol": "^1.10.3", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.2.7" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw=="], - - "pg-cloudflare": ["pg-cloudflare@1.2.7", "", {}, "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg=="], - - "pg-connection-string": ["pg-connection-string@2.9.1", "", {}, "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="], - - "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], - - "pg-pool": ["pg-pool@3.10.1", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg=="], - - "pg-protocol": ["pg-protocol@1.10.3", "", {}, "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="], - - "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "pino": ["pino@10.1.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w=="], + "pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="], "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], - "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + "pino-pretty": ["pino-pretty@11.3.0", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "readable-stream": "^4.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA=="], - "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], - - "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], - - "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], - - "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], - - "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + "pino-std-serializers": ["pino-std-serializers@7.1.0", "", {}, "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "sonic-boom": ["sonic-boom@4.2.1", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - "ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="], + "ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - "typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.4", "", {}, "sha512-Zw/uYiiyF6pUT1qmKbZziChgNPRu+ZRneAsMUDU6IwmXdWt5JwcUfy2bvLOCUtz5UniaN/Zx5aFttZYbYc7O/A=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } diff --git a/configs/dev.yaml b/configs/dev.yaml deleted file mode 100644 index 0004fdb..0000000 --- a/configs/dev.yaml +++ /dev/null @@ -1,41 +0,0 @@ -server: - port: 3000 - host: "0.0.0.0" - env: "dev" - -database: - host: "localhost" - port: 5432 - user: "postgres" - password: "postgres" - dbname: "postgres" - driver: "postgres" - max_conns: 25 - min_conns: 5 - max_conn_lifetime: 1h - max_conn_idletime: 10m - health_check_period: 1m -jwt: - secret: "dev-jwt-secret-key-change-in-production" - expiration: 3600 - -logging: - level: "debug" - format: "console" - -cors: - allowed_origins: - - "http://localhost:3000" - - "http://localhost:8080" - - "https://dev.example.com" - -rate_limit: - requests: 100 - window: 60 - -argon2id: - memory: 19456 - iterations: 2 - parallelism: 1 - salt_length: 16 - key_length: 32 diff --git a/configs/local.yaml b/configs/local.yaml deleted file mode 100644 index d4e3939..0000000 --- a/configs/local.yaml +++ /dev/null @@ -1,41 +0,0 @@ -server: - port: 3000 - host: "0.0.0.0" - env: "local" - -database: - driver: "postgres" - host: "localhost" - port: 5432 - user: "postgres" - password: "postgres" - dbname: "postgres" - max_conns: 25 - min_conns: 5 - max_conn_lifetime: 1h - max_conn_idletime: 10m - health_check_period: 1m - -jwt: - secret: "your-super-secret-jwt-key-for-development-only-123" - expiration: 3600 - -logging: - level: "debug" - format: "console" - -cors: - allowed_origins: - - "http://localhost:3000" - - "http://localhost:8080" - -rate_limit: - requests: 100 - window: 60 - -argon2id: - memory: 19456 - iterations: 2 - parallelism: 1 - salt_length: 16 - key_length: 32 diff --git a/configs/prod.yaml b/configs/prod.yaml deleted file mode 100644 index 4126fa2..0000000 --- a/configs/prod.yaml +++ /dev/null @@ -1,40 +0,0 @@ -server: - port: 3000 - host: "0.0.0.0" - env: "prod" - -database: - host: "prod-db.example.com" - port: 5432 - user: "prod_user" - password: "CHANGE_ME" - dbname: "zercle_db_prod" - driver: "postgres" - max_conns: 25 - min_conns: 5 - max_conn_lifetime: 1h - max_conn_idletime: 10m - health_check_period: 1m - -jwt: - secret: "" # Must be set via JWT_SECRET environment variable - expiration: 3600 - -logging: - level: "info" - format: "json" - -cors: - allowed_origins: - - "https://app.example.com" - -rate_limit: - requests: 100 - window: 60 - -argon2id: - memory: 19456 - iterations: 2 - parallelism: 1 - salt_length: 16 - key_length: 32 diff --git a/configs/uat.yaml b/configs/uat.yaml deleted file mode 100644 index d83e181..0000000 --- a/configs/uat.yaml +++ /dev/null @@ -1,40 +0,0 @@ -server: - port: 3000 - host: "0.0.0.0" - env: "uat" - -database: - host: "uat-db.example.com" - port: 5432 - user: "uat_user" - password: "CHANGE_ME" - dbname: "zercle_db_uat" - driver: "postgres" - max_conns: 25 - min_conns: 5 - max_conn_lifetime: 1h - max_conn_idletime: 10m - health_check_period: 1m - -jwt: - secret: "CHANGE_ME_UAT_SECRET" - expiration: 3600 - -logging: - level: "info" - format: "json" - -cors: - allowed_origins: - - "https://uat.example.com" - -rate_limit: - requests: 200 - window: 60 - -argon2id: - memory: 19456 - iterations: 2 - parallelism: 1 - salt_length: 16 - key_length: 32 diff --git a/deployments/docker/Dockerfile b/deployments/docker/Dockerfile deleted file mode 100644 index 8d889ac..0000000 --- a/deployments/docker/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -# Build stage -FROM mirror.gcr.io/oven/bun:latest AS builder - -# Install build dependencies -RUN apt-get update && apt-get install -y git make tini - -WORKDIR /app - -# Install dependencies -COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile - -# Copy source code -COPY . . - -# Compile your application into a single executable binary -RUN bun build src/main.ts --compile --minify --outfile ./dist/bun-app - -# Final stage - distroless minimal runtime -FROM gcr.io/distroless/base AS runner - -ENV TZ=Asia/Bangkok -ENV LANG=C.UTF-8 - -WORKDIR /opt/app - -# Copy tini static binary from builder -COPY --from=builder /usr/bin/tini /usr/bin/tini - -# Copy the compiled binary from the builder stage -COPY --from=builder /app/dist ./ - -# Expose port -EXPOSE 3000 - -# Run the application -ENTRYPOINT ["/usr/bin/tini", "--"] - -CMD ["./bun-app"] diff --git a/deployments/docker/docker-compose.test.yml b/deployments/docker/docker-compose.test.yml deleted file mode 100644 index dcf05b6..0000000 --- a/deployments/docker/docker-compose.test.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: "3.8" - -# Integration test database configuration -# Uses Podman-compatible settings with auto-cleanup for test environments - -services: - test-db: - image: postgres:18-alpine - container_name: zercle-test-db - environment: - POSTGRES_USER: test_user - POSTGRES_PASSWORD: test_password - POSTGRES_DB: test_db - ports: - - "5432:5432" - volumes: - - test-db-data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"] - interval: 2s - timeout: 5s - retries: 10 - start_period: 10s - # Auto-remove container when it stops (useful for CI/test pipelines) - restart: "no" - # Podman-specific: use host network for better compatibility - networks: - - test-network - -networks: - test-network: - driver: bridge - -volumes: - test-db-data: - driver: local diff --git a/deployments/docker/docker-compose.yml b/deployments/docker/docker-compose.yml deleted file mode 100644 index 903770e..0000000 --- a/deployments/docker/docker-compose.yml +++ /dev/null @@ -1,56 +0,0 @@ -services: - postgres: - image: mirror.gcr.io/postgres:18-alpine - container_name: zercle-postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 10s - timeout: 5s - retries: 5 - - app: - build: - context: ../.. - dockerfile: deployments/docker/Dockerfile - container_name: zercle-bun-app - environment: - SERVER_ENV: prod - SERVER_PORT: 3000 - SERVER_HOST: 0.0.0.0 - DATABASE_HOST: postgres - DATABASE_PORT: 5432 - DATABASE_USER: postgres - DATABASE_PASSWORD: postgres - DATABASE_NAME: postgres - DATABASE_DRIVER: postgres - JWT_SECRET: ${JWT_SECRET:-change-this-in-production} - JWT_EXPIRATION: 3600 - LOG_LEVEL: info - LOG_FORMAT: json - RATE_LIMIT_REQUESTS: 100 - RATE_LIMIT_WINDOW: 60 - ARGON2ID_MEMORY: 19456 - ARGON2ID_ITERATIONS: 2 - ARGON2ID_PARALLELISM: 1 - ports: - - "3000:3000" - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: ["CMD-SHELL", "wget -q --spider http://localhost:3000 || exit 1"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - -volumes: - postgres_data: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bcff795 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,63 @@ +version: "3.8" + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: ${APP_NAME:-hono-bun-app} + ports: + - "${APP_PORT:-3000}:3000" + environment: + - NODE_ENV=development + - PORT=3000 + - LOG_LEVEL=${LOG_LEVEL:-info} + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + - /app/.bun/install + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + networks: + - app-network + labels: + - "com.docker.compose.project=${APP_NAME:-hono-bun}" + - "com.docker.compose.service=app" + + # Development server with hot reload + app-dev: + build: + context: . + dockerfile: Dockerfile + container_name: ${APP_NAME:-hono-bun-app}-dev + ports: + - "${APP_PORT:-3000}:3000" + - "${DEBUG_PORT:-9229}:9229" + environment: + - NODE_ENV=development + - PORT=3000 + - LOG_LEVEL=debug + - BUN_ENV=development + env_file: + - .env + volumes: + - .:/app + - /app/node_modules + - /app/.bun/install + command: bun run --watch dev + restart: unless-stopped + profiles: + - dev + networks: + - app-network + +networks: + app-network: + driver: bridge diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index 32a25ca..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Config } from "drizzle-kit"; -import "dotenv/config"; - -export default { - schema: "./src/infrastructure/db/drizzle.ts", - out: "./drizzle/migrations", - dialect: "postgresql", - dbCredentials: { - connectionString: - process.env.DATABASE_URL || - "postgres://postgres:postgres@localhost:5432/postgres", - }, -} satisfies Config; diff --git a/drizzle/migrations/0001_initial_schema.sql b/drizzle/migrations/0001_initial_schema.sql deleted file mode 100644 index 0b9c401..0000000 --- a/drizzle/migrations/0001_initial_schema.sql +++ /dev/null @@ -1,31 +0,0 @@ --- Create users table -CREATE TABLE IF NOT EXISTS users ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - full_name VARCHAR(255) NOT NULL, - phone VARCHAR(20), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- Create tasks table -CREATE TABLE IF NOT EXISTS tasks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - title VARCHAR(255) NOT NULL, - description TEXT, - status VARCHAR(50) NOT NULL DEFAULT 'pending', - priority VARCHAR(50) NOT NULL DEFAULT 'medium', - due_date TIMESTAMP, - completed_at TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW() -); - --- Create indexes -CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); -CREATE INDEX IF NOT EXISTS idx_tasks_user_id ON tasks(user_id); -CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); -CREATE INDEX IF NOT EXISTS idx_tasks_created_at ON tasks(created_at DESC); diff --git a/drizzle/migrations/0002_nebulous_tomas.sql b/drizzle/migrations/0002_nebulous_tomas.sql deleted file mode 100644 index 39237a3..0000000 --- a/drizzle/migrations/0002_nebulous_tomas.sql +++ /dev/null @@ -1,26 +0,0 @@ -CREATE TABLE "tasks" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "user_id" uuid NOT NULL, - "title" varchar(255) NOT NULL, - "description" text, - "status" varchar(50) DEFAULT 'pending' NOT NULL, - "priority" varchar(50) DEFAULT 'medium' NOT NULL, - "due_date" timestamp, - "completed_at" timestamp, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL -); ---> statement-breakpoint -CREATE TABLE "users" ( - "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, - "email" varchar(255) NOT NULL, - "password" varchar(255) NOT NULL, - "full_name" varchar(255) NOT NULL, - "phone" varchar(20), - "is_active" boolean DEFAULT true NOT NULL, - "created_at" timestamp DEFAULT now() NOT NULL, - "updated_at" timestamp DEFAULT now() NOT NULL, - CONSTRAINT "users_email_unique" UNIQUE("email") -); ---> statement-breakpoint -ALTER TABLE "tasks" ADD CONSTRAINT "tasks_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/migrations/meta/0002_snapshot.json b/drizzle/migrations/meta/0002_snapshot.json deleted file mode 100644 index 9a623ef..0000000 --- a/drizzle/migrations/meta/0002_snapshot.json +++ /dev/null @@ -1,178 +0,0 @@ -{ - "id": "ede4f862-44e1-4d01-ade0-c20618672775", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.tasks": { - "name": "tasks", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "status": { - "name": "status", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "default": "'pending'" - }, - "priority": { - "name": "priority", - "type": "varchar(50)", - "primaryKey": false, - "notNull": true, - "default": "'medium'" - }, - "due_date": { - "name": "due_date", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "completed_at": { - "name": "completed_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": { - "tasks_user_id_users_id_fk": { - "name": "tasks_user_id_users_id_fk", - "tableFrom": "tasks", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "email": { - "name": "email", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "password": { - "name": "password", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "full_name": { - "name": "full_name", - "type": "varchar(255)", - "primaryKey": false, - "notNull": true - }, - "phone": { - "name": "phone", - "type": "varchar(20)", - "primaryKey": false, - "notNull": false - }, - "is_active": { - "name": "is_active", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_email_unique": { - "name": "users_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/migrations/meta/_journal.json b/drizzle/migrations/meta/_journal.json deleted file mode 100644 index af551e9..0000000 --- a/drizzle/migrations/meta/_journal.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "7", - "dialect": "postgres", - "entries": [ - { - "idx": 1, - "version": "1", - "when": "20250102_000000_000", - "tag": "0001_initial_schema", - "breakpoints": true - }, - { - "idx": 2, - "version": "7", - "when": 1767328773821, - "tag": "0002_nebulous_tomas", - "breakpoints": true - } - ] -} diff --git a/eslint.config.js b/eslint.config.js index 9e4a1e6..014072f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,87 +1,67 @@ -import tseslint from "typescript-eslint"; -import prettierConfig from "eslint-config-prettier"; +/** + * ESLint Flat Configuration + * + * This configuration provides: + * - TypeScript ESLint for TypeScript support + * - Prettier integration via eslint-config-prettier + * - Recommended rules for TypeScript projects + * - Import/export rules + */ + +import typescriptEslint from '@typescript-eslint/eslint-plugin' +import typescriptParser from '@typescript-eslint/parser' +import eslintConfigPrettier from 'eslint-config-prettier' export default [ // Ignore patterns { ignores: [ - "node_modules/", - "dist/", - "build/", - ".bun/", - "drizzle/", - "*.config.*", - ".env*", - "*.log", - "*.sql", + '**/node_modules/**', + '**/dist/**', + '**/build/**', + '**/.git/**', + '**/*.min.js', + '**/bun.lockb', + 'coverage/**', ], }, - // TypeScript files (not tests) + // TypeScript files { - files: ["src/**/*.ts", "scripts/**/*.ts"], + files: ['**/*.ts', '**/*.tsx'], plugins: { - "@typescript-eslint": tseslint.plugin, + '@typescript-eslint': typescriptEslint, }, languageOptions: { - parser: tseslint.parser, + parser: typescriptParser, parserOptions: { - project: "./tsconfig.json", + ecmaVersion: 'latest', + sourceType: 'module', + project: './tsconfig.json', tsconfigRootDir: import.meta.dirname, - sourceType: "module", }, }, rules: { - // TypeScript specific rules (aligned with strict tsconfig) - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-non-null-assertion": "warn", - "@typescript-eslint/consistent-type-imports": "error", - "@typescript-eslint/consistent-type-exports": "error", - "@typescript-eslint/prefer-nullish-coalescing": "error", - "@typescript-eslint/prefer-optional-chain": "error", - }, - }, + // TypeScript ESLint recommended rules + ...typescriptEslint.configs.recommended.rules, - // Test files - { - files: ["tests/**/*.ts"], - plugins: { - "@typescript-eslint": tseslint.plugin, - }, - languageOptions: { - parser: tseslint.parser, - parserOptions: { - sourceType: "module", - }, - globals: { - Bun: "readonly", - test: "readonly", - describe: "readonly", - it: "readonly", - expect: "readonly", - beforeAll: "readonly", - afterAll: "readonly", - beforeEach: "readonly", - afterEach: "readonly", - }, - }, - rules: { - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-unused-vars": "off", + // Additional TypeScript rules for strict checking + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], + '@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + + // General rules + 'no-console': 'off', // Allow console in development + 'no-debugger': 'warn', + 'prefer-const': 'error', + 'object-shorthand': 'error', }, }, - // Apply prettier last to avoid conflicts - prettierConfig, -]; + // Apply prettier config last to override any conflicting rules + eslintConfigPrettier, +] diff --git a/package.json b/package.json index df325d7..7374579 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,59 @@ { "name": "zercle-bun-template", "version": "1.0.0", - "description": "Production-ready RESTful API template built with Bun runtime and Hono framework", - "type": "module", + "description": "Production-ready Hono.js + Bun template for building web APIs", + "author": "Zercle", + "license": "MIT", + "keywords": [ + "hono", + "bun", + "typescript", + "api", + "web", + "framework" + ], + "repository": { + "type": "git", + "url": "https://github.com/zercle/zercle-bun-template" + }, "scripts": { - "dev": "bun run --watch src/main.ts", - "start": "bun run src/main.ts", - "build": "bun build src/main.ts --target bun --outdir ./dist", - "test": "bun test", - "test:integration": "bun test --envfile .env.test tests/integration/", - "test:coverage": "bun test --coverage", - "test:db:start": "podman-compose -f deployments/docker/docker-compose.test.yml up -d", - "test:db:stop": "podman-compose -f deployments/docker/docker-compose.test.yml down -v", - "test:db:migrate": "bunx drizzle-kit migrate --config drizzle.config.ts", - "test:db:health": "bun tests/integration/setup.ts --health-check", + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "build": "bun build src/index.ts --outdir ./build --target bun", + "typecheck": "tsc --noEmit", "lint": "eslint .", "lint:fix": "eslint . --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "db:generate": "bunx drizzle-kit generate", - "db:push": "bunx drizzle-kit push", - "db:studio": "bunx drizzle-kit studio", - "db:migrate": "bunx drizzle-kit migrate" + "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"", + "test": "bun test", + "test:watch": "bun test --watch", + "test:coverage": "bun test --coverage" }, "dependencies": { - "@hono/node-server": "^1.19.7", - "@hono/zod-validator": "^0.7.6", - "argon2": "^0.44.0", - "dotenv": "^17.2.3", - "drizzle-orm": "^0.45.1", - "hono": "^4.11.3", - "js-yaml": "^4.1.1", - "jsonwebtoken": "^9.0.3", - "pg": "^8.16.3", - "pino": "^10.1.0", - "postgres": "^3.4.7", - "uuid": "^13.0.0", - "zod": "^4.3.4" + "hono": "^4.0.0", + "zod": "^3.22.0", + "@hono/zod-validator": "^0.4.0", + "pino": "^9.0.0", + "pino-pretty": "^11.0.0", + "dotenv": "^16.4.0" }, "devDependencies": { - "@types/js-yaml": "^4.0.9", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^25.0.3", - "@types/pg": "^8.16.0", - "@types/uuid": "^11.0.0", - "@typescript-eslint/eslint-plugin": "^8.51.0", - "@typescript-eslint/parser": "^8.51.0", - "bun-types": "^1.3.5", - "drizzle-kit": "^0.31.8", - "esbuild": "^0.27.2", - "eslint": "^9.39.2", - "eslint-config-prettier": "^10.1.8", - "prettier": "^3.7.4", - "typescript": "^5.9.3", - "typescript-eslint": "^8.51.0" + "@types/bun": "^1.0.0", + "@types/node": "^20.11.0", + "typescript": "^5.3.0", + "bun-types": "^1.0.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "prettier": "^3.2.0", + "@typescript-eslint/eslint-plugin": "^7.0.0", + "@typescript-eslint/parser": "^7.0.0" }, "engines": { "bun": ">=1.0.0" }, - "overrides": { - "esbuild": ">=0.25.0" - } + "trustedDependencies": [ + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "eslint" + ] } diff --git a/src/app.ts b/src/app.ts deleted file mode 100644 index 31b1499..0000000 --- a/src/app.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Hono } from "hono"; -import { serve } from "@hono/node-server"; -import type { Config } from "./infrastructure/config/config.js"; -import { DrizzleDatabase } from "./infrastructure/db/drizzle.js"; -import { Logger } from "./infrastructure/logger/logger.js"; -import { Passworder } from "./infrastructure/password/passworder.js"; -import { createRequestIDMiddleware } from "./infrastructure/middleware/request-id.js"; -import { createLoggerMiddleware } from "./infrastructure/middleware/logger.js"; -import { createCorsMiddleware } from "./infrastructure/middleware/cors.js"; -import { createRateLimitMiddleware } from "./infrastructure/middleware/rate-limit.js"; -import { createAuthMiddleware } from "./infrastructure/middleware/auth.js"; -import { UserRepository } from "./domain/user/repository/user.js"; -import { UserUseCase } from "./domain/user/usecase/user.js"; -import { UserHandler } from "./domain/user/handler/user.js"; -import { TaskRepository } from "./domain/task/repository/task.js"; -import { TaskUseCase } from "./domain/task/usecase/task.js"; -import { TaskHandler } from "./domain/task/handler/task.js"; -import { success } from "./utils/response.js"; - -export class App { - private hono: Hono; - private db: DrizzleDatabase; - private logger: Logger; - private config: Config; - private server: ReturnType; - - constructor(config: Config) { - this.config = config; - this.logger = new Logger(config.logging); - this.db = new DrizzleDatabase(config.database); - this.hono = new Hono(); - - this.setupMiddleware(); - this.setupDependencies(); - this.setupRoutes(); - } - - private setupMiddleware() { - // Request ID middleware (first) - this.hono.use("*", createRequestIDMiddleware()); - - // Logger middleware - this.hono.use("*", createLoggerMiddleware(this.logger)); - - // CORS middleware - this.hono.use("*", createCorsMiddleware(this.config.cors)); - - // Rate limiting middleware - this.hono.use("*", createRateLimitMiddleware(this.config.rate_limit)); - } - - private setupDependencies() { - // User domain - const userRepo = new UserRepository(this.db, this.logger); - const passworder = new Passworder(this.config.argon2id); - const userUseCase = new UserUseCase( - userRepo, - this.config.jwt, - passworder, - this.logger, - ); - const userHandler = new UserHandler(userUseCase, this.logger); - - // Task domain - const taskRepo = new TaskRepository(this.db, this.logger); - const taskUseCase = new TaskUseCase(taskRepo, this.logger); - const taskHandler = new TaskHandler(taskUseCase, this.logger); - - // Register routes - this.registerUserRoutes(userHandler); - this.registerTaskRoutes(taskHandler); - } - - private setupRoutes() { - // Health check endpoints - this.hono.get("/health", (c) => { - return success(c, { - status: "healthy", - timestamp: new Date().toISOString(), - }); - }); - - this.hono.get("/readiness", async (c) => { - const isHealthy = await this.db.healthCheck(); - if (isHealthy) { - return success(c, { status: "ready" }); - } - return c.json({ status: "error", message: "Database not ready" }, 503); - }); - } - - private registerUserRoutes(handler: UserHandler) { - // Public routes - this.hono.post("/api/v1/auth/register", (c) => handler.register(c)); - this.hono.post("/api/v1/auth/login", (c) => handler.login(c)); - - // Protected routes - const protectedRoutes = this.hono.basePath("/api/v1"); - protectedRoutes.use("*", createAuthMiddleware(this.config.jwt)); - - protectedRoutes.get("/users/profile", (c) => handler.getProfile(c)); - protectedRoutes.put("/users/profile", (c) => handler.updateProfile(c)); - protectedRoutes.delete("/users/profile", (c) => handler.deleteAccount(c)); - protectedRoutes.get("/users", (c) => handler.listUsers(c)); - } - - private registerTaskRoutes(handler: TaskHandler) { - // All task routes are protected - const protectedRoutes = this.hono.basePath("/api/v1"); - protectedRoutes.use("/tasks*", createAuthMiddleware(this.config.jwt)); - - protectedRoutes.post("/tasks", (c) => handler.createTask(c)); - protectedRoutes.get("/tasks", (c) => handler.listTasks(c)); - protectedRoutes.get("/tasks/:id", (c) => handler.getTask(c)); - protectedRoutes.put("/tasks/:id", (c) => handler.updateTask(c)); - protectedRoutes.delete("/tasks/:id", (c) => handler.deleteTask(c)); - } - - async start(): Promise { - const address = `${this.config.server.host}:${this.config.server.port}`; - - this.logger.info("Starting server", { - host: this.config.server.host, - port: this.config.server.port, - env: this.config.server.env, - }); - - this.server = serve({ - fetch: this.hono.fetch, - port: this.config.server.port, - hostname: this.config.server.host, - }); - - this.logger.info("Server started", { address }); - } - - async stop(): Promise { - this.logger.info("Shutting down server..."); - - if (this.server) { - this.server.close(); - } - - await this.db.close(); - - this.logger.info("Server stopped"); - } - - getHono(): Hono { - return this.hono; - } -} diff --git a/src/config/env.ts b/src/config/env.ts new file mode 100644 index 0000000..27689d2 --- /dev/null +++ b/src/config/env.ts @@ -0,0 +1,107 @@ +/** + * Environment Configuration Module + * + * This module handles environment variable validation and configuration + * using Zod for schema validation. It ensures all required environment + * variables are present and properly typed at application startup. + */ + +import { z } from 'zod' + +/** + * Environment variable schema definition + * + * This schema defines all required and optional environment variables + * with their types, defaults, and validation rules. + */ +const envSchema = z.object({ + // Application + NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), + PORT: z.coerce.number().default(3000), + HOST: z.string().default('0.0.0.0'), + APP_NAME: z.string().default('Hono API'), + APP_VERSION: z.string().default('1.0.0'), + + // CORS + CORS_ORIGIN: z.string().default('*'), + + // Database + DATABASE_URL: z.string().url().optional(), + DATABASE_POOL_MIN: z.coerce.number().default(2), + DATABASE_POOL_MAX: z.coerce.number().default(10), + + // Authentication + JWT_SECRET: z.string().min(32).optional(), + JWT_EXPIRES_IN: z.string().default('1h'), + + // Logging + LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'), + + // Rate Limiting + RATE_LIMIT_WINDOW_MS: z.coerce.number().default(60000), + RATE_LIMIT_MAX_REQUESTS: z.coerce.number().default(100), + + // Redis (optional) + REDIS_URL: z.string().url().optional(), + + // External Services + API_KEY_EXTERNAL: z.string().optional(), +}) + +/** + * Type definition inferred from the environment schema + * Used throughout the application for type-safe access to environment variables + */ +export type Env = z.infer + +/** + * Load and validate environment variables + * + * This function reads environment variables from process.env, + * validates them against the schema, and returns a typed object. + * Throws an error if validation fails. + * + * @returns Validated environment configuration + * @throws {z.ZodError} If environment variables fail validation + */ +export function loadEnv(): Env { + const env = { + NODE_ENV: process.env.NODE_ENV, + PORT: process.env.PORT, + HOST: process.env.HOST, + APP_NAME: process.env.APP_NAME, + APP_VERSION: process.env.APP_VERSION, + CORS_ORIGIN: process.env.CORS_ORIGIN, + DATABASE_URL: process.env.DATABASE_URL, + DATABASE_POOL_MIN: process.env.DATABASE_POOL_MIN, + DATABASE_POOL_MAX: process.env.DATABASE_POOL_MAX, + JWT_SECRET: process.env.JWT_SECRET, + JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN, + LOG_LEVEL: process.env.LOG_LEVEL, + RATE_LIMIT_WINDOW_MS: process.env.RATE_LIMIT_WINDOW_MS, + RATE_LIMIT_MAX_REQUESTS: process.env.RATE_LIMIT_MAX_REQUESTS, + REDIS_URL: process.env.REDIS_URL, + API_KEY_EXTERNAL: process.env.API_KEY_EXTERNAL, + } + + return envSchema.parse(env) +} + +/** + * Singleton instance of the validated environment configuration + * Lazy initialized to avoid issues during module loading + */ +let _config: Env | null = null + +/** + * Get the environment configuration singleton + * Initializes and caches the configuration on first access + * + * @returns The validated environment configuration + */ +export function getEnv(): Env { + if (_config === null) { + _config = loadEnv() + } + return _config +} diff --git a/src/domain/task/entity/task.ts b/src/domain/task/entity/task.ts deleted file mode 100644 index 8194b78..0000000 --- a/src/domain/task/entity/task.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Task { - id: string; - userId: string; - title: string; - description?: string; - status: "pending" | "in_progress" | "completed" | "cancelled"; - priority: "low" | "medium" | "high" | "urgent"; - dueDate?: Date; - completedAt?: Date; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateTask { - userId: string; - title: string; - description?: string; - status?: "pending" | "in_progress" | "completed" | "cancelled"; - priority?: "low" | "medium" | "high" | "urgent"; - dueDate?: Date; -} - -export interface UpdateTask { - title?: string; - description?: string; - status?: "pending" | "in_progress" | "completed" | "cancelled"; - priority?: "low" | "medium" | "high" | "urgent"; - dueDate?: Date; -} diff --git a/src/domain/task/handler/task.ts b/src/domain/task/handler/task.ts deleted file mode 100644 index 835c114..0000000 --- a/src/domain/task/handler/task.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { Context } from "hono"; -import type { ITaskService } from "../usecase/task.js"; -import { ErrTaskNotFound, ErrUnauthorizedTask } from "../usecase/task.js"; -import { createTaskSchema, updateTaskSchema } from "../request/task.js"; -import { - success, - created, - noContent, - badRequest, - notFound, - forbidden, - internalError, -} from "../../../utils/response.js"; -import { getUserId } from "../../../infrastructure/middleware/auth.js"; -import { getRequestID } from "../../../infrastructure/middleware/request-id.js"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; - -export class TaskHandler { - constructor( - private useCase: ITaskService, - private logger: Logger, - ) {} - - async createTask(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - - if (!userId) { - return forbidden(c, "Authentication required"); - } - - try { - const body = await c.req.json(); - const validatedData = createTaskSchema.parse(body); - - const result = await this.useCase.createTask(userId, validatedData); - - this.logger.info("Task created successfully", { - request_id: requestId, - user_id: userId, - task_id: result.id, - }); - - return created(c, result); - } catch (error) { - if (error instanceof Error && error.name === "ZodError") { - return badRequest(c, "Validation failed", this.formatZodError(error)); - } - this.logger.error("Failed to create task", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to create task"); - } - } - - async getTask(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - const taskId = c.req.param("id"); - - if (!userId) { - return forbidden(c, "Authentication required"); - } - - try { - const result = await this.useCase.getTask(taskId, userId); - - return success(c, result); - } catch (error) { - if (error instanceof ErrTaskNotFound) { - return notFound(c, "Task not found"); - } - if (error instanceof ErrUnauthorizedTask) { - return forbidden(c, "You do not have access to this task"); - } - this.logger.error("Failed to get task", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to get task"); - } - } - - async listTasks(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - - if (!userId) { - return forbidden(c, "Authentication required"); - } - - try { - const limit = this.parseLimit(c.req.query("limit")); - const offset = this.parseOffset(c.req.query("offset")); - - const result = await this.useCase.listTasks(userId, limit, offset); - - return success(c, result, { limit, offset }); - } catch (error) { - this.logger.error("Failed to list tasks", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to list tasks"); - } - } - - async updateTask(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - const taskId = c.req.param("id"); - - if (!userId) { - return forbidden(c, "Authentication required"); - } - - try { - const body = await c.req.json(); - const validatedData = updateTaskSchema.parse(body); - - const result = await this.useCase.updateTask( - taskId, - userId, - validatedData, - ); - - this.logger.info("Task updated successfully", { - request_id: requestId, - user_id: userId, - task_id: taskId, - }); - - return success(c, result); - } catch (error) { - if (error instanceof ErrTaskNotFound) { - return notFound(c, "Task not found"); - } - if (error instanceof ErrUnauthorizedTask) { - return forbidden(c, "You do not have access to this task"); - } - if (error instanceof Error && error.name === "ZodError") { - return badRequest(c, "Validation failed", this.formatZodError(error)); - } - this.logger.error("Failed to update task", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to update task"); - } - } - - async deleteTask(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - const taskId = c.req.param("id"); - - if (!userId) { - return forbidden(c, "Authentication required"); - } - - try { - await this.useCase.deleteTask(taskId, userId); - - this.logger.info("Task deleted successfully", { - request_id: requestId, - user_id: userId, - task_id: taskId, - }); - - return noContent(c); - } catch (error) { - if (error instanceof ErrTaskNotFound) { - return notFound(c, "Task not found"); - } - if (error instanceof ErrUnauthorizedTask) { - return forbidden(c, "You do not have access to this task"); - } - this.logger.error("Failed to delete task", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to delete task"); - } - } - - private parseLimit(limit?: string): number { - const parsed = parseInt(limit ?? "20", 10); - if (parsed <= 0 || parsed > 100) { - return 20; - } - return parsed; - } - - private parseOffset(offset?: string): number { - const parsed = parseInt(offset ?? "0", 10); - if (parsed < 0) { - return 0; - } - return parsed; - } - - private formatZodError(error: Error): Record { - if (error.name === "ZodError" && "issues" in error) { - const issues = ( - error as { issues: Array<{ path: string[]; message: string }> } - ).issues; - const errors: Record = {}; - - for (const issue of issues) { - const path = issue.path.join("."); - if (!errors[path]) { - errors[path] = []; - } - errors[path].push(issue.message); - } - - return errors; - } - - return {}; - } -} diff --git a/src/domain/task/repository/task.ts b/src/domain/task/repository/task.ts deleted file mode 100644 index 75f2401..0000000 --- a/src/domain/task/repository/task.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { Task, CreateTask, UpdateTask } from "../entity/task.js"; -import type { DrizzleDatabase } from "../../../infrastructure/db/drizzle.js"; -import { tasks } from "../../../infrastructure/db/drizzle.js"; -import { eq, and, desc } from "drizzle-orm"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; - -type TaskStatus = "pending" | "in_progress" | "completed"; -type TaskPriority = "low" | "medium" | "high"; -type UpdateTaskData = { - updated_at: Date; - title?: string; - description?: string | null; - status?: TaskStatus; - priority?: TaskPriority; - due_date?: Date | null; - completed_at?: Date; -}; - -export interface ITaskRepository { - create(task: CreateTask): Promise; - getByID(id: string): Promise; - listByUser( - userId: string, - limit: number, - offset: number, - ): Promise<{ tasks: Task[]; total: number }>; - update(id: string, userId: string, task: UpdateTask): Promise; - delete(id: string, userId: string): Promise; -} - -export class TaskRepository implements ITaskRepository { - constructor( - private db: DrizzleDatabase, - private logger: Logger, - ) {} - - async create(task: CreateTask): Promise { - try { - const [newTask] = await this.db.db - .insert(tasks) - .values({ - user_id: task.userId, - title: task.title, - description: task.description ?? null, - status: task.status ?? "pending", - priority: task.priority ?? "medium", - due_date: task.dueDate ?? null, - }) - .returning(); - - return { - id: newTask.id, - userId: newTask.user_id, - title: newTask.title, - description: newTask.description ?? undefined, - status: newTask.status as TaskStatus, - priority: newTask.priority as TaskPriority, - dueDate: newTask.due_date ?? undefined, - completedAt: newTask.completed_at ?? undefined, - createdAt: newTask.created_at, - updatedAt: newTask.updated_at, - }; - } catch (error) { - this.logger.error("Failed to create task", { error }); - throw error; - } - } - - async getByID(id: string): Promise { - try { - const task = await this.db.db - .select() - .from(tasks) - .where(eq(tasks.id, id)) - .limit(1); - - if (task.length === 0) { - return null; - } - - const t = task[0]; - return { - id: t.id, - userId: t.user_id, - title: t.title, - description: t.description ?? undefined, - status: t.status as TaskStatus, - priority: t.priority as TaskPriority, - dueDate: t.due_date ?? undefined, - completedAt: t.completed_at ?? undefined, - createdAt: t.created_at, - updatedAt: t.updated_at, - }; - } catch (error) { - this.logger.error("Failed to get task by ID", { error, id }); - throw error; - } - } - - async listByUser( - userId: string, - limit: number, - offset: number, - ): Promise<{ tasks: Task[]; total: number }> { - try { - const taskList = await this.db.db - .select() - .from(tasks) - .where(eq(tasks.user_id, userId)) - .orderBy(desc(tasks.created_at)) - .limit(limit) - .offset(offset); - - const totalResult = await this.db.db - .select({ count: tasks.id }) - .from(tasks) - .where(eq(tasks.user_id, userId)); - const total = totalResult.length; - - const tasksList: Task[] = taskList.map((t) => ({ - id: t.id, - userId: t.user_id, - title: t.title, - description: t.description ?? undefined, - status: t.status as TaskStatus, - priority: t.priority as TaskPriority, - dueDate: t.due_date ?? undefined, - completedAt: t.completed_at ?? undefined, - createdAt: t.created_at, - updatedAt: t.updated_at, - })); - - return { tasks: tasksList, total }; - } catch (error) { - this.logger.error("Failed to list tasks", { error, userId }); - throw error; - } - } - - async update( - id: string, - userId: string, - task: UpdateTask, - ): Promise { - try { - const updateData: UpdateTaskData = { - updated_at: new Date(), - }; - - if (task.title !== undefined) updateData.title = task.title; - if (task.description !== undefined) - updateData.description = task.description; - if (task.status !== undefined) { - updateData.status = task.status; - if (task.status === "completed") { - updateData.completed_at = new Date(); - } - } - if (task.priority !== undefined) updateData.priority = task.priority; - if (task.dueDate !== undefined) updateData.due_date = task.dueDate; - - const [updatedTask] = await this.db.db - .update(tasks) - .set(updateData) - .where(and(eq(tasks.id, id), eq(tasks.user_id, userId))) - .returning(); - - if (!updatedTask) { - return null; - } - - return { - id: updatedTask.id, - userId: updatedTask.user_id, - title: updatedTask.title, - description: updatedTask.description ?? undefined, - status: updatedTask.status as TaskStatus, - priority: updatedTask.priority as TaskPriority, - dueDate: updatedTask.due_date ?? undefined, - completedAt: updatedTask.completed_at ?? undefined, - createdAt: updatedTask.created_at, - updatedAt: updatedTask.updated_at, - }; - } catch (error) { - this.logger.error("Failed to update task", { error, id, userId }); - throw error; - } - } - - async delete(id: string, userId: string): Promise { - try { - await this.db.db - .delete(tasks) - .where(and(eq(tasks.id, id), eq(tasks.user_id, userId))); - } catch (error) { - this.logger.error("Failed to delete task", { error, id, userId }); - throw error; - } - } -} diff --git a/src/domain/task/request/task.ts b/src/domain/task/request/task.ts deleted file mode 100644 index 74099e5..0000000 --- a/src/domain/task/request/task.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; - -export const createTaskSchema = z.object({ - title: z.string().min(1, "Title is required"), - description: z.string().optional(), - status: z - .enum(["pending", "in_progress", "completed", "cancelled"]) - .optional(), - priority: z.enum(["low", "medium", "high", "urgent"]).optional(), - dueDate: z.coerce.date().optional(), -}); - -export const updateTaskSchema = z.object({ - title: z.string().min(1, "Title is required").optional(), - description: z.string().optional(), - status: z - .enum(["pending", "in_progress", "completed", "cancelled"]) - .optional(), - priority: z.enum(["low", "medium", "high", "urgent"]).optional(), - dueDate: z.coerce.date().optional(), -}); - -export type CreateTask = z.infer; -export type UpdateTask = z.infer; diff --git a/src/domain/task/response/task.ts b/src/domain/task/response/task.ts deleted file mode 100644 index 832cb2e..0000000 --- a/src/domain/task/response/task.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface TaskResponse { - id: string; - userId: string; - title: string; - description?: string; - status: "pending" | "in_progress" | "completed" | "cancelled"; - priority: "low" | "medium" | "high" | "urgent"; - dueDate?: Date; - completedAt?: Date; - createdAt: Date; - updatedAt: Date; -} - -export interface ListTasksResponse { - tasks: TaskResponse[]; - total: number; -} diff --git a/src/domain/task/usecase/task.ts b/src/domain/task/usecase/task.ts deleted file mode 100644 index d1c1e84..0000000 --- a/src/domain/task/usecase/task.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { CreateTask, UpdateTask } from "../entity/task.js"; -import type { ITaskRepository } from "../repository/task.js"; -import type { - CreateTask as CreateTaskDTO, - UpdateTask as UpdateTaskDTO, -} from "../request/task.js"; -import type { TaskResponse, ListTasksResponse } from "../response/task.js"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; - -export class ErrTaskNotFound extends Error { - constructor() { - super("Task not found"); - this.name = "ErrTaskNotFound"; - } -} - -export class ErrUnauthorizedTask extends Error { - constructor() { - super("Unauthorized access to task"); - this.name = "ErrUnauthorizedTask"; - } -} - -export interface ITaskService { - createTask(userId: string, task: CreateTaskDTO): Promise; - getTask(id: string, userId: string): Promise; - listTasks( - userId: string, - limit: number, - offset: number, - ): Promise; - updateTask( - id: string, - userId: string, - task: UpdateTaskDTO, - ): Promise; - deleteTask(id: string, userId: string): Promise; -} - -export class TaskUseCase implements ITaskService { - constructor( - private repo: ITaskRepository, - private logger: Logger, - ) {} - - async createTask(userId: string, task: CreateTaskDTO): Promise { - const createTask: CreateTask = { - userId, - title: task.title, - description: task.description, - status: task.status, - priority: task.priority, - dueDate: task.dueDate, - }; - - const created = await this.repo.create(createTask); - - return { - id: created.id, - userId: created.userId, - title: created.title, - description: created.description, - status: created.status, - priority: created.priority, - dueDate: created.dueDate, - completedAt: created.completedAt, - createdAt: created.createdAt, - updatedAt: created.updatedAt, - }; - } - - async getTask(id: string, userId: string): Promise { - const task = await this.repo.getByID(id); - if (!task) { - throw new ErrTaskNotFound(); - } - - // Check ownership - if (task.userId !== userId) { - throw new ErrUnauthorizedTask(); - } - - return { - id: task.id, - userId: task.userId, - title: task.title, - description: task.description, - status: task.status, - priority: task.priority, - dueDate: task.dueDate, - completedAt: task.completedAt, - createdAt: task.createdAt, - updatedAt: task.updatedAt, - }; - } - - async listTasks( - userId: string, - limit: number, - offset: number, - ): Promise { - const { tasks, total } = await this.repo.listByUser(userId, limit, offset); - - return { - tasks: tasks.map((t) => ({ - id: t.id, - userId: t.userId, - title: t.title, - description: t.description, - status: t.status, - priority: t.priority, - dueDate: t.dueDate, - completedAt: t.completedAt, - createdAt: t.createdAt, - updatedAt: t.updatedAt, - })), - total, - }; - } - - async updateTask( - id: string, - userId: string, - task: UpdateTaskDTO, - ): Promise { - // Check if task exists and belongs to user - const existing = await this.repo.getByID(id); - if (!existing) { - throw new ErrTaskNotFound(); - } - - if (existing.userId !== userId) { - throw new ErrUnauthorizedTask(); - } - - const updateTask: UpdateTask = { - title: task.title, - description: task.description, - status: task.status, - priority: task.priority, - dueDate: task.dueDate, - }; - - const updated = await this.repo.update(id, userId, updateTask); - - return { - id: updated.id, - userId: updated.userId, - title: updated.title, - description: updated.description, - status: updated.status, - priority: updated.priority, - dueDate: updated.dueDate, - completedAt: updated.completedAt, - createdAt: updated.createdAt, - updatedAt: updated.updatedAt, - }; - } - - async deleteTask(id: string, userId: string): Promise { - // Check if task exists and belongs to user - const existing = await this.repo.getByID(id); - if (!existing) { - throw new ErrTaskNotFound(); - } - - if (existing.userId !== userId) { - throw new ErrUnauthorizedTask(); - } - - await this.repo.delete(id, userId); - } -} diff --git a/src/domain/user/entity/user.ts b/src/domain/user/entity/user.ts deleted file mode 100644 index c682080..0000000 --- a/src/domain/user/entity/user.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface User { - id: string; - email: string; - password: string; - fullName: string; - phone?: string; - isActive: boolean; - createdAt: Date; - updatedAt: Date; -} - -export interface CreateUser { - email: string; - password: string; - fullName: string; - phone?: string; -} - -export interface UpdateUser { - fullName?: string; - phone?: string; -} diff --git a/src/domain/user/handler/user.ts b/src/domain/user/handler/user.ts deleted file mode 100644 index e79bdbe..0000000 --- a/src/domain/user/handler/user.ts +++ /dev/null @@ -1,236 +0,0 @@ -import type { Context } from "hono"; -import type { IUserService } from "../usecase/user.js"; -import { - ErrUserNotFound, - ErrUserAlreadyExists, - ErrInvalidCredentials, -} from "../usecase/user.js"; -import { - registerUserSchema, - loginUserSchema, - updateUserSchema, -} from "../request/user.js"; -import { - success, - created, - noContent, - badRequest, - unauthorized, - notFound, - conflict, - internalError, -} from "../../../utils/response.js"; -import { getUserId } from "../../../infrastructure/middleware/auth.js"; -import { getRequestID } from "../../../infrastructure/middleware/request-id.js"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; - -export class UserHandler { - constructor( - private useCase: IUserService, - private logger: Logger, - ) {} - - async register(c: Context) { - const requestId = getRequestID(c); - - try { - const body = await c.req.json(); - const validatedData = registerUserSchema.parse(body); - - const result = await this.useCase.register(validatedData); - - this.logger.info("User registered successfully", { - request_id: requestId, - email: validatedData.email, - }); - - return created(c, result); - } catch (error) { - if (error instanceof ErrUserAlreadyExists) { - return conflict(c, "Email already exists"); - } - if (error instanceof Error && error.name === "ZodError") { - return badRequest(c, "Validation failed", this.formatZodError(error)); - } - this.logger.error("Failed to register user", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to register user"); - } - } - - async login(c: Context) { - const requestId = getRequestID(c); - - try { - const body = await c.req.json(); - const validatedData = loginUserSchema.parse(body); - - const result = await this.useCase.login(validatedData); - - this.logger.info("User logged in successfully", { - request_id: requestId, - email: validatedData.email, - }); - - return success(c, result); - } catch (error) { - if (error instanceof ErrInvalidCredentials) { - return unauthorized(c, "Invalid email or password"); - } - if (error instanceof Error && error.name === "ZodError") { - return badRequest(c, "Validation failed", this.formatZodError(error)); - } - this.logger.error("Failed to login user", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to login user"); - } - } - - async getProfile(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - - if (!userId) { - return unauthorized(c, "Invalid token"); - } - - try { - const result = await this.useCase.getProfile(userId); - - return success(c, result); - } catch (error) { - if (error instanceof ErrUserNotFound) { - return notFound(c, "User not found"); - } - this.logger.error("Failed to get user profile", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to get user profile"); - } - } - - async updateProfile(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - - if (!userId) { - return unauthorized(c, "Invalid token"); - } - - try { - const body = await c.req.json(); - const validatedData = updateUserSchema.parse(body); - - const result = await this.useCase.updateProfile(userId, validatedData); - - this.logger.info("User profile updated successfully", { - request_id: requestId, - user_id: userId, - }); - - return success(c, result); - } catch (error) { - if (error instanceof ErrUserNotFound) { - return notFound(c, "User not found"); - } - if (error instanceof Error && error.name === "ZodError") { - return badRequest(c, "Validation failed", this.formatZodError(error)); - } - this.logger.error("Failed to update user profile", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to update user profile"); - } - } - - async deleteAccount(c: Context) { - const requestId = getRequestID(c); - const userId = getUserId(c); - - if (!userId) { - return unauthorized(c, "Invalid token"); - } - - try { - await this.useCase.deleteAccount(userId); - - this.logger.info("User account deleted successfully", { - request_id: requestId, - user_id: userId, - }); - - return noContent(c); - } catch (error) { - if (error instanceof ErrUserNotFound) { - return notFound(c, "User not found"); - } - this.logger.error("Failed to delete user account", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to delete user account"); - } - } - - async listUsers(c: Context) { - const requestId = getRequestID(c); - - try { - const limit = this.parseLimit(c.req.query("limit")); - const offset = this.parseOffset(c.req.query("offset")); - - const result = await this.useCase.listUsers(limit, offset); - - return success(c, result, { limit, offset }); - } catch (error) { - this.logger.error("Failed to list users", { - request_id: requestId, - error: error instanceof Error ? error.message : String(error), - }); - return internalError(c, "Failed to list users"); - } - } - - private parseLimit(limit?: string): number { - const parsed = parseInt(limit ?? "20", 10); - if (parsed <= 0 || parsed > 100) { - return 20; - } - return parsed; - } - - private parseOffset(offset?: string): number { - const parsed = parseInt(offset ?? "0", 10); - if (parsed < 0) { - return 0; - } - return parsed; - } - - private formatZodError(error: Error): Record { - if (error.name === "ZodError" && "issues" in error) { - const issues = ( - error as { issues: Array<{ path: string[]; message: string }> } - ).issues; - const errors: Record = {}; - - for (const issue of issues) { - const path = issue.path.join("."); - if (!errors[path]) { - errors[path] = []; - } - errors[path].push(issue.message); - } - - return errors; - } - - return {}; - } -} diff --git a/src/domain/user/repository/user.ts b/src/domain/user/repository/user.ts deleted file mode 100644 index febe18c..0000000 --- a/src/domain/user/repository/user.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { User, CreateUser, UpdateUser } from "../entity/user.js"; -import type { DrizzleDatabase } from "../../../infrastructure/db/drizzle.js"; -import { users } from "../../../infrastructure/db/drizzle.js"; -import { eq, desc } from "drizzle-orm"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; - -export interface IUserRepository { - create(user: CreateUser): Promise; - getByID(id: string): Promise; - getByEmail(email: string): Promise; - update(id: string, user: UpdateUser): Promise; - delete(id: string): Promise; - list( - limit: number, - offset: number, - ): Promise<{ users: User[]; total: number }>; -} - -export class UserRepository implements IUserRepository { - constructor( - private db: DrizzleDatabase, - private logger: Logger, - ) {} - - async create(user: CreateUser): Promise { - try { - const [newUser] = await this.db.db - .insert(users) - .values({ - email: user.email, - password: user.password, - full_name: user.fullName, - phone: user.phone ?? null, - is_active: true, - }) - .returning(); - - return { - id: newUser.id, - email: newUser.email, - password: newUser.password, - fullName: newUser.full_name, - phone: newUser.phone ?? undefined, - isActive: newUser.is_active, - createdAt: newUser.created_at, - updatedAt: newUser.updated_at, - }; - } catch (error) { - this.logger.error("Failed to create user", { error }); - throw error; - } - } - - async getByID(id: string): Promise { - try { - const user = await this.db.db - .select() - .from(users) - .where(eq(users.id, id)) - .limit(1); - - if (user.length === 0) { - return null; - } - - const u = user[0]; - return { - id: u.id, - email: u.email, - password: u.password, - fullName: u.full_name, - phone: u.phone ?? undefined, - isActive: u.is_active, - createdAt: u.created_at, - updatedAt: u.updated_at, - }; - } catch (error) { - this.logger.error("Failed to get user by ID", { error, id }); - throw error; - } - } - - async getByEmail(email: string): Promise { - try { - const user = await this.db.db - .select() - .from(users) - .where(eq(users.email, email)) - .limit(1); - - if (user.length === 0) { - return null; - } - - const u = user[0]; - return { - id: u.id, - email: u.email, - password: u.password, - fullName: u.full_name, - phone: u.phone ?? undefined, - isActive: u.is_active, - createdAt: u.created_at, - updatedAt: u.updated_at, - }; - } catch (error) { - this.logger.error("Failed to get user by email", { error, email }); - throw error; - } - } - - async update(id: string, user: UpdateUser): Promise { - try { - const [updatedUser] = await this.db.db - .update(users) - .set({ - full_name: user.fullName, - phone: user.phone ?? null, - updated_at: new Date(), - }) - .where(eq(users.id, id)) - .returning(); - - if (!updatedUser) { - return null; - } - - return { - id: updatedUser.id, - email: updatedUser.email, - password: updatedUser.password, - fullName: updatedUser.full_name, - phone: updatedUser.phone ?? undefined, - isActive: updatedUser.is_active, - createdAt: updatedUser.created_at, - updatedAt: updatedUser.updated_at, - }; - } catch (error) { - this.logger.error("Failed to update user", { error, id }); - throw error; - } - } - - async delete(id: string): Promise { - try { - await this.db.db.delete(users).where(eq(users.id, id)); - } catch (error) { - this.logger.error("Failed to delete user", { error, id }); - throw error; - } - } - - async list( - limit: number, - offset: number, - ): Promise<{ users: User[]; total: number }> { - try { - const userList = await this.db.db - .select() - .from(users) - .orderBy(desc(users.created_at)) - .limit(limit) - .offset(offset); - - const totalResult = await this.db.db - .select({ count: users.id }) - .from(users); - const total = totalResult.length; - - const usersList: User[] = userList.map((u) => ({ - id: u.id, - email: u.email, - password: u.password, - fullName: u.full_name, - phone: u.phone ?? undefined, - isActive: u.is_active, - createdAt: u.created_at, - updatedAt: u.updated_at, - })); - - return { users: usersList, total }; - } catch (error) { - this.logger.error("Failed to list users", { error }); - throw error; - } - } -} diff --git a/src/domain/user/request/user.ts b/src/domain/user/request/user.ts deleted file mode 100644 index 717b487..0000000 --- a/src/domain/user/request/user.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from "zod"; - -export const registerUserSchema = z.object({ - email: z.string().email("Invalid email format"), - password: z.string().min(8, "Password must be at least 8 characters"), - fullName: z.string().min(2, "Full name must be at least 2 characters"), - phone: z.string().optional(), -}); - -export const loginUserSchema = z.object({ - email: z.string().email("Invalid email format"), - password: z.string().min(1, "Password is required"), -}); - -export const updateUserSchema = z.object({ - fullName: z - .string() - .min(2, "Full name must be at least 2 characters") - .optional(), - phone: z.string().optional(), -}); - -export type RegisterUser = z.infer; -export type LoginUser = z.infer; -export type UpdateUser = z.infer; diff --git a/src/domain/user/response/user.ts b/src/domain/user/response/user.ts deleted file mode 100644 index b119c3e..0000000 --- a/src/domain/user/response/user.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface UserResponse { - id: string; - email: string; - fullName: string; - phone?: string; - createdAt: Date; - updatedAt: Date; -} - -export interface LoginResponse { - token: string; - user: UserResponse; -} - -export interface ListUsersResponse { - users: UserResponse[]; - total: number; -} diff --git a/src/domain/user/usecase/user.ts b/src/domain/user/usecase/user.ts deleted file mode 100644 index 37c064e..0000000 --- a/src/domain/user/usecase/user.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { CreateUser, UpdateUser } from "../entity/user.js"; -import type { IUserRepository } from "../repository/user.js"; -import type { RegisterUser, LoginUser } from "../request/user.js"; -import type { - UserResponse, - LoginResponse, - ListUsersResponse, -} from "../response/user.js"; -import type { JWTConfig } from "../../../infrastructure/config/config.js"; -import type { Passworder } from "../../../infrastructure/password/passworder.js"; -import type { Logger } from "../../../infrastructure/logger/logger.js"; -import { generateToken } from "../../../infrastructure/middleware/auth.js"; - -export class ErrUserNotFound extends Error { - constructor() { - super("User not found"); - this.name = "ErrUserNotFound"; - } -} - -export class ErrUserAlreadyExists extends Error { - constructor() { - super("User already exists"); - this.name = "ErrUserAlreadyExists"; - } -} - -export class ErrInvalidCredentials extends Error { - constructor() { - super("Invalid credentials"); - this.name = "ErrInvalidCredentials"; - } -} - -export interface IUserService { - register(user: RegisterUser): Promise; - login(user: LoginUser): Promise; - getProfile(id: string): Promise; - updateProfile(id: string, user: UpdateUser): Promise; - deleteAccount(id: string): Promise; - listUsers(limit: number, offset: number): Promise; -} - -export class UserUseCase implements IUserService { - constructor( - private repo: IUserRepository, - private jwtConfig: JWTConfig, - private passworder: Passworder, - private logger: Logger, - ) {} - - async register(user: RegisterUser): Promise { - // Check if user already exists - const existing = await this.repo.getByEmail(user.email); - if (existing) { - throw new ErrUserAlreadyExists(); - } - - // Hash password - const hashedPassword = await this.passworder.hashPassword(user.password); - - // Create user - const newUser: CreateUser = { - email: user.email, - password: hashedPassword, - fullName: user.fullName, - phone: user.phone, - }; - - const created = await this.repo.create(newUser); - - // Generate token - const token = generateToken(created.id, created.email, this.jwtConfig); - - return { - token, - user: { - id: created.id, - email: created.email, - fullName: created.fullName, - phone: created.phone, - createdAt: created.createdAt, - updatedAt: created.updatedAt, - }, - }; - } - - async login(user: LoginUser): Promise { - // Get user by email - const existing = await this.repo.getByEmail(user.email); - if (!existing) { - throw new ErrInvalidCredentials(); - } - - // Verify password - const isValid = await this.passworder.verifyPassword( - user.password, - existing.password, - ); - if (!isValid) { - throw new ErrInvalidCredentials(); - } - - // Generate token - const token = generateToken(existing.id, existing.email, this.jwtConfig); - - return { - token, - user: { - id: existing.id, - email: existing.email, - fullName: existing.fullName, - phone: existing.phone, - createdAt: existing.createdAt, - updatedAt: existing.updatedAt, - }, - }; - } - - async getProfile(id: string): Promise { - const user = await this.repo.getByID(id); - if (!user) { - throw new ErrUserNotFound(); - } - - return { - id: user.id, - email: user.email, - fullName: user.fullName, - phone: user.phone, - createdAt: user.createdAt, - updatedAt: user.updatedAt, - }; - } - - async updateProfile(id: string, user: UpdateUser): Promise { - // Validate full name length if provided - if (user.fullName && user.fullName.length < 2) { - throw new Error("Full name must be at least 2 characters"); - } - - const updated = await this.repo.update(id, user); - if (!updated) { - throw new ErrUserNotFound(); - } - - return { - id: updated.id, - email: updated.email, - fullName: updated.fullName, - phone: updated.phone, - createdAt: updated.createdAt, - updatedAt: updated.updatedAt, - }; - } - - async deleteAccount(id: string): Promise { - const user = await this.repo.getByID(id); - if (!user) { - throw new ErrUserNotFound(); - } - - await this.repo.delete(id); - } - - async listUsers(limit: number, offset: number): Promise { - const { users, total } = await this.repo.list(limit, offset); - - return { - users: users.map((u) => ({ - id: u.id, - email: u.email, - fullName: u.fullName, - phone: u.phone, - createdAt: u.createdAt, - updatedAt: u.updatedAt, - })), - total, - }; - } -} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..47ce71b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,217 @@ +/** + * Hono.js + Bun Application Entry Point + * + * This is the main entry point for the application. It sets up: + * - Hono app with all middleware + * - CORS configuration + * - Request logging + * - Error handling + * - Graceful shutdown handling + * - Server initialization + */ + +import 'dotenv/config' +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { secureHeaders } from 'hono/secure-headers' +import { getEnv } from './config/env.ts' +import { requestLogger, getLogger } from './middleware/logger.ts' +import { errorHandler } from './middleware/error-handler.ts' +import { notFoundHandler as customNotFoundHandler } from './middleware/not-found.ts' +import routes from './routes/index.ts' +import type { AppEnv } from './types/index.ts' + +/** + * Create and configure the Hono application + * + * This function sets up the application with all middleware and routes. + * It follows the middleware chain order defined in the architecture document. + * + * @returns Configured Hono application instance + */ +function createApp(): Hono { + const app = new Hono() + + /** + * CORS middleware + * Configured with origin from environment variables + */ + const env = getEnv() + app.use( + cors({ + origin: env.CORS_ORIGIN, + credentials: true, + allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'], + }) + ) + + /** + * Security headers middleware + * Adds various security headers to responses + */ + app.use( + secureHeaders({ + contentSecurityPolicy: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'"], + imgSrc: ["'self'", 'data:'], + }, + crossOriginEmbedderPolicy: false, + crossOriginOpenerPolicy: false, + crossOriginResourcePolicy: false, + originAgentCluster: false, + referrerPolicy: false, + strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload', + xContentTypeOptions: 'nosniff', + xDnsPrefetchControl: false, + xDownloadOptions: 'noopen', + xFrameOptions: 'DENY', + xPermittedCrossDomainPolicies: 'none', + xXssProtection: '1; mode=block', + }) + ) + + /** + * Request logging middleware + * Logs all incoming requests and responses + */ + app.use(requestLogger()) + + /** + * Mount application routes + * All routes are mounted under the root path + */ + app.route('/', routes) + + /** + * Error handling middleware + * Catches and formats all errors consistently + */ + app.onError(errorHandler) + + /** + * Not found handler + * Returns 404 for unmatched routes + */ + app.notFound(customNotFoundHandler) + + return app +} + +/** + * Start the HTTP server + * + * This function creates the app, starts the Bun server, + * and sets up graceful shutdown handlers. + */ +async function startServer(): Promise { + const env = getEnv() + const logger = getLogger() + + // Create the application + const app = createApp() + + // Create the Bun server + const server = Bun.serve({ + port: env.PORT, + hostname: env.HOST, + fetch: app.fetch, + + /** + * Request error handler + * Catches errors during request processing + */ + error(error: Error) { + logger.error({ + event: 'server_error', + error: error.message, + stack: error.stack, + }) + return new Response('Internal Server Error', { status: 500 }) + }, + }) + + // Log server startup + const startupMessage = `🚀 Server running at http://${server.hostname}:${server.port}` + logger.info({ + event: 'server_started', + port: server.port, + hostname: server.hostname, + env: env.NODE_ENV, + version: env.APP_VERSION, + }) + console.log(startupMessage) + + /** + * Graceful shutdown handler + * + * Handles SIGTERM and SIGINT signals to gracefully + * shut down the server. It stops accepting new + * connections and allows in-flight requests to complete. + * + * @param signal - The signal that triggered the shutdown + */ + async function shutdown(signal: string): Promise { + logger.info({ + event: 'shutdown_started', + signal, + }) + console.log(`\n${signal} received. Starting graceful shutdown...`) + + // Stop accepting new connections + server.stop() + + // Wait for in-flight requests to complete (max 30 seconds) + const shutdownTimeout = 30000 + const checkInterval = 1000 + let waited = 0 + + while (waited < shutdownTimeout) { + // Check if there are active connections + // Bun doesn't expose active connection count directly + // We'll use a simple timeout approach + await new Promise((resolve) => setTimeout(resolve, checkInterval)) + waited += checkInterval + } + + logger.info({ + event: 'shutdown_completed', + signal, + shutdownTimeout, + }) + console.log('Graceful shutdown completed.') + + process.exit(0) + } + + // Register shutdown handlers + process.on('SIGTERM', () => shutdown('SIGTERM')) + process.on('SIGINT', () => shutdown('SIGINT')) + + // Handle uncaught exceptions + process.on('uncaughtException', (error: Error) => { + logger.error({ + event: 'uncaught_exception', + error: error.message, + stack: error.stack, + }) + shutdown('uncaughtException') + }) + + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason: unknown) => { + logger.error({ + event: 'unhandled_rejection', + reason, + }) + shutdown('unhandledRejection') + }) +} + +// Start the server +startServer().catch((error) => { + console.error('Failed to start server:', error) + process.exit(1) +}) diff --git a/src/infrastructure/config/config.ts b/src/infrastructure/config/config.ts deleted file mode 100644 index 8ef3add..0000000 --- a/src/infrastructure/config/config.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { readFileSync } from "fs"; -import { resolve } from "path"; -import yaml from "js-yaml"; -import { z } from "zod"; - -// Configuration schemas with validation -const ServerConfigSchema = z.object({ - host: z.string().min(1), - port: z.number().int().min(1).max(65535), - env: z.string(), -}); - -const DatabaseConfigSchema = z.object({ - host: z.string().min(1), - port: z.number().int().min(1).max(65535), - user: z.string().min(1), - password: z.string().min(1), - dbname: z.string().min(1), - driver: z.string().default("postgres"), - max_conns: z.number().int().positive().default(25), - min_conns: z.number().int().nonnegative().default(5), - max_conn_lifetime: z.string().default("1h"), - max_conn_idletime: z.string().default("10m"), - health_check_period: z.string().default("1m"), -}); - -const JWTConfigSchema = z.object({ - secret: z.string().min(1), - expiration: z.number().int().positive().default(3600), -}); - -const LoggingConfigSchema = z.object({ - level: z.enum(["debug", "info", "warn", "error", "fatal"]).default("info"), - format: z.enum(["console", "json"]).default("json"), -}); - -const CORSConfigSchema = z.object({ - allowed_origins: z.array(z.string()).default(["*"]), -}); - -const RateLimitConfigSchema = z.object({ - requests: z.number().int().positive().default(100), - window: z.number().int().positive().default(60), -}); - -const Argon2idConfigSchema = z.object({ - memory: z.number().int().positive().default(19456), - iterations: z.number().int().positive().default(2), - parallelism: z.number().int().positive().default(1), - salt_length: z.number().int().positive().default(16), - key_length: z.number().int().positive().default(32), -}); - -const ConfigSchema = z.object({ - server: ServerConfigSchema, - database: DatabaseConfigSchema, - jwt: JWTConfigSchema, - logging: LoggingConfigSchema, - cors: CORSConfigSchema, - rate_limit: RateLimitConfigSchema, - argon2id: Argon2idConfigSchema, -}); - -export interface ServerConfig { - host: string; - port: number; - env: string; -} - -export interface DatabaseConfig { - host: string; - port: number; - user: string; - password: string; - dbname: string; - driver: string; - max_conns: number; - min_conns: number; - max_conn_lifetime: string; - max_conn_idletime: string; - health_check_period: string; -} - -export interface JWTConfig { - secret: string; - expiration: number; -} - -export interface LoggingConfig { - level: "debug" | "info" | "warn" | "error" | "fatal"; - format: "console" | "json"; -} - -export interface CORSConfig { - allowed_origins: string[]; -} - -export interface RateLimitConfig { - requests: number; - window: number; -} - -export interface Argon2idConfig { - memory: number; - iterations: number; - parallelism: number; - salt_length: number; - key_length: number; -} - -export interface Config { - server: ServerConfig; - database: DatabaseConfig; - jwt: JWTConfig; - logging: LoggingConfig; - cors: CORSConfig; - rate_limit: RateLimitConfig; - argon2id: Argon2idConfig; -} - -/** - * Load configuration from YAML file and override with environment variables - */ -export function loadConfig(configPath?: string): Config { - const env = process.env.SERVER_ENV ?? "local"; - const configFilePath = - configPath ?? resolve(process.cwd(), `configs/${env}.yaml`); - - try { - const fileContents = readFileSync(configFilePath, "utf8"); - const config = yaml.load(fileContents) as Record; - - // Override with environment variables - if (process.env.SERVER_HOST) config.server.host = process.env.SERVER_HOST; - if (process.env.SERVER_PORT) - config.server.port = parseInt(process.env.SERVER_PORT, 10); - if (process.env.SERVER_ENV) config.server.env = process.env.SERVER_ENV; - - if (process.env.DATABASE_HOST) - config.database.host = process.env.DATABASE_HOST; - else if (process.env.DB_HOST) config.database.host = process.env.DB_HOST; - if (process.env.DATABASE_PORT) - config.database.port = parseInt(process.env.DATABASE_PORT, 10); - else if (process.env.DB_PORT) - config.database.port = parseInt(process.env.DB_PORT, 10); - if (process.env.DATABASE_USER) - config.database.user = process.env.DATABASE_USER; - else if (process.env.DB_USER) config.database.user = process.env.DB_USER; - if (process.env.DATABASE_PASSWORD) - config.database.password = process.env.DATABASE_PASSWORD; - else if (process.env.DB_PASSWORD) - config.database.password = process.env.DB_PASSWORD; - if (process.env.DATABASE_NAME) - config.database.dbname = process.env.DATABASE_NAME; - else if (process.env.DB_NAME) config.database.dbname = process.env.DB_NAME; - if (process.env.DATABASE_DRIVER) - config.database.driver = process.env.DATABASE_DRIVER; - else if (process.env.DB_DRIVER) - config.database.driver = process.env.DB_DRIVER; - - if (process.env.JWT_SECRET) config.jwt.secret = process.env.JWT_SECRET; - if (process.env.JWT_EXPIRATION) - config.jwt.expiration = parseInt(process.env.JWT_EXPIRATION, 10); - - if (process.env.LOG_LEVEL) - config.logging.level = process.env.LOG_LEVEL as LoggingConfig["level"]; - if (process.env.LOG_FORMAT) - config.logging.format = process.env.LOG_FORMAT as LoggingConfig["format"]; - - if (process.env.ARGON2ID_MEMORY) - config.argon2id.memory = parseInt(process.env.ARGON2ID_MEMORY, 10); - if (process.env.ARGON2ID_ITERATIONS) - config.argon2id.iterations = parseInt( - process.env.ARGON2ID_ITERATIONS, - 10, - ); - if (process.env.ARGON2ID_PARALLELISM) - config.argon2id.parallelism = parseInt( - process.env.ARGON2ID_PARALLELISM, - 10, - ); - - if (process.env.RATE_LIMIT_REQUESTS) - config.rate_limit.requests = parseInt( - process.env.RATE_LIMIT_REQUESTS, - 10, - ); - if (process.env.RATE_LIMIT_WINDOW) - config.rate_limit.window = parseInt(process.env.RATE_LIMIT_WINDOW, 10); - - // Validate configuration - return ConfigSchema.parse(config); - } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to load configuration: ${error.message}`); - } - throw error; - } -} diff --git a/src/infrastructure/db/drizzle.ts b/src/infrastructure/db/drizzle.ts deleted file mode 100644 index 7e42732..0000000 --- a/src/infrastructure/db/drizzle.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { drizzle } from "drizzle-orm/postgres-js"; -import postgres from "postgres"; -import { - pgTable, - uuid, - varchar, - text, - timestamp, - boolean, -} from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; - -// Users table -export const users = pgTable("users", { - id: uuid("id").primaryKey().defaultRandom(), - email: varchar("email", { length: 255 }).notNull().unique(), - password: varchar("password", { length: 255 }).notNull(), - full_name: varchar("full_name", { length: 255 }).notNull(), - phone: varchar("phone", { length: 20 }), - is_active: boolean("is_active").notNull().default(true), - created_at: timestamp("created_at").notNull().defaultNow(), - updated_at: timestamp("updated_at").notNull().defaultNow(), -}); - -// Tasks table -export const tasks = pgTable("tasks", { - id: uuid("id").primaryKey().defaultRandom(), - user_id: uuid("user_id") - .notNull() - .references(() => users.id, { onDelete: "cascade" }), - title: varchar("title", { length: 255 }).notNull(), - description: text("description"), - status: varchar("status", { length: 50 }).notNull().default("pending"), - priority: varchar("priority", { length: 50 }).notNull().default("medium"), - due_date: timestamp("due_date"), - completed_at: timestamp("completed_at"), - created_at: timestamp("created_at").notNull().defaultNow(), - updated_at: timestamp("updated_at").notNull().defaultNow(), -}); - -// Types for tables -export type User = typeof users.$inferSelect; -export type NewUser = typeof users.$inferInsert; -export type Task = typeof tasks.$inferSelect; -export type NewTask = typeof tasks.$inferInsert; - -export interface DatabaseConfig { - host: string; - port: number; - user: string; - password: string; - dbname: string; - max_conns: number; - min_conns: number; - max_conn_lifetime: string; - max_conn_idletime: string; - health_check_period: string; -} - -export class DrizzleDatabase { - private client: postgres.Sql; - public db: ReturnType; - - constructor(config: DatabaseConfig) { - const connectionString = `postgres://${config.user}:${config.password}@${config.host}:${config.port}/${config.dbname}`; - - this.client = postgres(connectionString, { - max: config.max_conns, - min: config.min_conns, - idle_timeout: parseDuration(config.max_conn_idletime), - connect_timeout: parseDuration(config.max_conn_lifetime), - max_lifetime: parseDuration(config.max_conn_lifetime), - }); - - this.db = drizzle(this.client); - } - - async close(): Promise { - await this.client.end(); - } - - async healthCheck(): Promise { - try { - await this.db.execute(sql`SELECT 1`); - return true; - } catch { - return false; - } - } -} - -function parseDuration(duration: string): number { - const match = duration.match(/^(\d+)([smh])$/); - if (!match) { - return 60000; // default 1 minute - } - - const value = parseInt(match[1], 10); - const unit = match[2]; - - switch (unit) { - case "s": - return value * 1000; - case "m": - return value * 60 * 1000; - case "h": - return value * 60 * 60 * 1000; - default: - return 60000; - } -} diff --git a/src/infrastructure/logger/logger.ts b/src/infrastructure/logger/logger.ts deleted file mode 100644 index 1c20bc8..0000000 --- a/src/infrastructure/logger/logger.ts +++ /dev/null @@ -1,62 +0,0 @@ -import pino from "pino"; - -export interface LoggerConfig { - level: "debug" | "info" | "warn" | "error" | "fatal"; - format: "console" | "json"; -} - -export class Logger { - private logger: pino.Logger; - - constructor(config: LoggerConfig) { - const options: pino.LoggerOptions = { - level: config.level, - formatters: { - level: (label) => { - return { level: label }; - }, - }, - timestamp: pino.stdTimeFunctions.isoTime, - }; - - if (config.format === "console") { - options.transport = { - target: "pino-pretty", - options: { - colorize: true, - translateTime: "HH:MM:ss Z", - ignore: "pid,hostname", - }, - }; - } - - this.logger = pino(options); - } - - debug(message: string, meta?: Record): void { - this.logger.debug(meta ?? {}, message); - } - - info(message: string, meta?: Record): void { - this.logger.info(meta ?? {}, message); - } - - warn(message: string, meta?: Record): void { - this.logger.warn(meta ?? {}, message); - } - - error(message: string, meta?: Record): void { - this.logger.error(meta ?? {}, message); - } - - fatal(message: string, meta?: Record): void { - this.logger.fatal(meta ?? {}, message); - } - - child(bindings: Record): Logger { - const childLogger = this.logger.child(bindings); - const child = new Logger({ level: "info", format: "json" }); - child.logger = childLogger; - return child; - } -} diff --git a/src/infrastructure/middleware/auth.ts b/src/infrastructure/middleware/auth.ts deleted file mode 100644 index 323a071..0000000 --- a/src/infrastructure/middleware/auth.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Context, Next } from "hono"; -import jwt from "jsonwebtoken"; -import type { JWTConfig } from "../config/config.js"; - -export interface JWTPayload { - userId: string; - email: string; - iat: number; - exp: number; -} - -export interface AuthContext { - userId: string; - email: string; -} - -export function createAuthMiddleware(config: JWTConfig) { - return async (c: Context, next: Next) => { - const authHeader = c.req.header("Authorization"); - - if (!authHeader?.startsWith("Bearer ")) { - return c.json( - { status: "error", message: "Missing or invalid authorization header" }, - 401, - ); - } - - const token = authHeader.substring(7); - - try { - const payload = jwt.verify(token, config.secret) as JWTPayload; - - // Add user info to context - c.set("userId", payload.userId); - c.set("email", payload.email); - - await next(); - } catch (error) { - if (error instanceof jwt.TokenExpiredError) { - return c.json({ status: "error", message: "Token expired" }, 401); - } - if (error instanceof jwt.JsonWebTokenError) { - return c.json({ status: "error", message: "Invalid token" }, 401); - } - return c.json({ status: "error", message: "Authentication failed" }, 401); - } - }; -} - -export function generateToken( - userId: string, - email: string, - config: JWTConfig, -): string { - const payload = { - userId, - email, - iat: Math.floor(Date.now() / 1000), - exp: Math.floor(Date.now() / 1000) + config.expiration, - }; - - return jwt.sign(payload, config.secret); -} - -export function getUserId(c: Context): string { - return c.get("userId") as string; -} - -export function getEmail(c: Context): string { - return c.get("email") as string; -} diff --git a/src/infrastructure/middleware/cors.ts b/src/infrastructure/middleware/cors.ts deleted file mode 100644 index 23d70ed..0000000 --- a/src/infrastructure/middleware/cors.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { cors } from "hono/cors"; -import type { CORSConfig } from "../config/config.js"; - -export function createCorsMiddleware(config: CORSConfig) { - return cors({ - origin: config.allowed_origins, - allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], - allowHeaders: ["Content-Type", "Authorization", "X-Request-ID"], - credentials: true, - maxAge: 86400, - }); -} diff --git a/src/infrastructure/middleware/logger.ts b/src/infrastructure/middleware/logger.ts deleted file mode 100644 index 0210b92..0000000 --- a/src/infrastructure/middleware/logger.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { Context, Next } from "hono"; -import type { Logger } from "../logger/logger.js"; -import { getRequestID } from "./request-id.js"; - -export function createLoggerMiddleware(logger: Logger) { - return async (c: Context, next: Next) => { - const start = Date.now(); - const requestId = getRequestID(c); - - logger.info("Request started", { - request_id: requestId, - method: c.req.method, - path: c.req.path, - query: c.req.query(), - }); - - await next(); - - const duration = Date.now() - start; - const status = c.res.status; - - logger.info("Request completed", { - request_id: requestId, - method: c.req.method, - path: c.req.path, - status, - duration: `${duration}ms`, - }); - }; -} diff --git a/src/infrastructure/middleware/rate-limit.ts b/src/infrastructure/middleware/rate-limit.ts deleted file mode 100644 index 8fa6f77..0000000 --- a/src/infrastructure/middleware/rate-limit.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Context, Next } from "hono"; -import type { RateLimitConfig } from "../config/config.js"; - -interface RateLimitEntry { - count: number; - resetTime: number; -} - -const rateLimitMap = new Map(); - -export function createRateLimitMiddleware(config: RateLimitConfig) { - return async (c: Context, next: Next) => { - const clientId = - c.req.header("X-Forwarded-For") ?? c.req.header("X-Real-IP") ?? "unknown"; - const now = Date.now(); - const windowMs = config.window * 1000; - - let entry = rateLimitMap.get(clientId); - - if (!entry || now > entry.resetTime) { - entry = { - count: 0, - resetTime: now + windowMs, - }; - rateLimitMap.set(clientId, entry); - } - - entry.count++; - - const remaining = Math.max(0, config.requests - entry.count); - const resetTime = Math.ceil(entry.resetTime / 1000); - - c.header("X-RateLimit-Limit", config.requests.toString()); - c.header("X-RateLimit-Remaining", remaining.toString()); - c.header("X-RateLimit-Reset", resetTime.toString()); - - if (entry.count > config.requests) { - return c.json( - { - status: "error", - message: "Rate limit exceeded", - }, - 429, - ); - } - - await next(); - }; -} diff --git a/src/infrastructure/middleware/request-id.ts b/src/infrastructure/middleware/request-id.ts deleted file mode 100644 index 2782b69..0000000 --- a/src/infrastructure/middleware/request-id.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Context, Next } from "hono"; -import { randomUUID } from "crypto"; - -export function createRequestIDMiddleware() { - return async (c: Context, next: Next) => { - const requestId = c.req.header("X-Request-ID") ?? randomUUID(); - - c.header("X-Request-ID", requestId); - c.set("requestId", requestId); - - await next(); - }; -} - -export function getRequestID(c: Context): string { - return c.get("requestId") as string; -} diff --git a/src/infrastructure/password/passworder.ts b/src/infrastructure/password/passworder.ts deleted file mode 100644 index 4657c4d..0000000 --- a/src/infrastructure/password/passworder.ts +++ /dev/null @@ -1,44 +0,0 @@ -import * as argon2 from "argon2"; - -export interface Argon2idConfig { - memory: number; - iterations: number; - parallelism: number; - salt_length: number; - key_length: number; -} - -export class Passworder { - private config: Argon2idConfig; - - constructor(config: Argon2idConfig) { - this.config = config; - } - - async hashPassword(password: string): Promise { - try { - return await argon2.hash(password, { - type: argon2.argon2id, - memoryCost: this.config.memory, - timeCost: this.config.iterations, - parallelism: this.config.parallelism, - hashLength: this.config.key_length, - saltLength: this.config.salt_length, - }); - } catch (error) { - throw new Error( - `Failed to hash password: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } - - async verifyPassword(password: string, hash: string): Promise { - try { - return await argon2.verify(hash, password); - } catch (error) { - throw new Error( - `Failed to verify password: ${error instanceof Error ? error.message : String(error)}`, - ); - } - } -} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 01f7d7e..0000000 --- a/src/main.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { loadConfig } from "./infrastructure/config/config.js"; -import { App } from "./app.js"; -import "dotenv/config"; - -async function main() { - try { - // Load configuration - const config = loadConfig(); - - // Create and start application - const app = new App(config); - await app.start(); - - // Handle graceful shutdown - process.on("SIGTERM", async () => { - console.log("SIGTERM signal received"); - await app.stop(); - process.exit(0); - }); - - process.on("SIGINT", async () => { - console.log("SIGINT signal received"); - await app.stop(); - process.exit(0); - }); - } catch (error) { - console.error("Failed to start application:", error); - process.exit(1); - } -} - -main(); diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts new file mode 100644 index 0000000..e99dd3a --- /dev/null +++ b/src/middleware/error-handler.ts @@ -0,0 +1,148 @@ +/** + * Global Error Handler Middleware + * + * This middleware provides centralized error handling for the application. + * It catches all errors thrown in route handlers and formats consistent + * error responses. It handles Zod validation errors, HTTP exceptions, + * and unexpected errors with appropriate logging. + */ + +import type { ErrorHandler } from 'hono' +import { HTTPException } from 'hono/http-exception' +import { ZodError } from 'zod' +import type { AppContext } from '../types/index.ts' + +/** + * Create a standardized error response object + * + * @param code - Machine-readable error code + * @param message - Human-readable error message + * @param requestId - Unique request identifier for tracing + * @param details - Optional detailed error information + * @returns Formatted error response object + */ +function createErrorResponse( + code: string, + message: string, + requestId: string, + details?: unknown +) { + return { + error: { + code, + message, + details, + }, + meta: { + timestamp: new Date().toISOString(), + requestId, + }, + } +} + +/** + * Global error handler middleware + * + * This handler intercepts all errors and formats them consistently: + * - Zod validation errors: Returns 400 with field-level details + * - HTTP exceptions: Returns the exception's status code and message + * - Unknown errors: Returns 500 with generic message in production + * or full error details in development + * + * All errors are logged for observability + */ +export const errorHandler: ErrorHandler = (err: Error, c: AppContext) => { + // Get request ID from context or generate a placeholder + const requestId = c.get('requestId') || 'unknown' + + // Get logger from context if available + const logger = c.get('logger') + + // Handle Zod validation errors + if (err instanceof ZodError) { + const errorResponse = createErrorResponse( + 'VALIDATION_ERROR', + 'Invalid request data', + requestId, + err.errors.map((e) => ({ + field: e.path.join('.'), + message: e.message, + code: e.code, + })) + ) + + logger?.warn({ + error: 'Validation error', + details: errorResponse.error.details, + requestId, + path: c.req.path, + method: c.req.method, + }) + + return c.json(errorResponse, 400) + } + + // Handle Hono HTTP exceptions + if (err instanceof HTTPException) { + const errorResponse = createErrorResponse( + err.status.toString(), + err.message, + requestId + ) + + logger?.warn({ + error: 'HTTP exception', + status: err.status, + requestId, + path: c.req.path, + method: c.req.method, + }) + + return c.json(errorResponse, err.status) + } + + // Handle unknown errors + const isProduction = process.env.NODE_ENV === 'production' + + const errorResponse = createErrorResponse( + 'INTERNAL_SERVER_ERROR', + isProduction ? 'An unexpected error occurred' : err.message, + requestId, + isProduction ? undefined : err.stack + ) + + logger?.error({ + error: err.message, + stack: err.stack, + requestId, + path: c.req.path, + method: c.req.method, + }) + + return c.json(errorResponse, 500) +} + +/** + * Not-found handler middleware + * + * Returns a consistent 404 response for unmatched routes + */ +export const notFoundHandler = (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + + const errorResponse = createErrorResponse( + 'NOT_FOUND', + `Route ${c.req.method} ${c.req.path} not found`, + requestId + ) + + const logger = c.get('logger') + logger?.warn({ + error: 'Route not found', + path: c.req.path, + method: c.req.method, + requestId, + }) + + return c.json(errorResponse, 404) +} diff --git a/src/middleware/logger.ts b/src/middleware/logger.ts new file mode 100644 index 0000000..b7d51eb --- /dev/null +++ b/src/middleware/logger.ts @@ -0,0 +1,145 @@ +/** + * Request Logger Middleware + * + * This middleware provides structured request/response logging using Pino. + * It logs request details, timing, and response status for observability. + * Each request gets a unique ID for distributed tracing. + */ + +import pino from 'pino' +import type { AppContext } from '../types/index.ts' + +/** + * Logger configuration options + */ +type LoggerConfig = { + /** Minimum log level to output */ + level: string + /** Whether to use pretty printing (development only) */ + prettyPrint: boolean +} + +/** + * Create a logger instance with the given configuration + * + * @param config - Logger configuration options + * @returns Configured Pino logger instance + */ +function createLogger(config: LoggerConfig): pino.Logger { + const options: pino.LoggerOptions = { + level: config.level, + base: { + env: process.env.NODE_ENV || 'development', + app: process.env.APP_NAME || 'hono-api', + version: process.env.APP_VERSION || '1.0.0', + }, + timestamp: pino.stdTimeFunctions.isoTime, + } + + if (config.prettyPrint) { + options.transport = { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + } + + return pino(options) +} + +/** + * Logger configuration - reads from environment variables + */ +const loggerConfig: LoggerConfig = { + level: process.env.LOG_LEVEL || 'info', + prettyPrint: process.env.NODE_ENV !== 'production', +} + +/** + * Singleton logger instance + */ +let _logger: pino.Logger | null = null + +/** + * Get the logger singleton instance + * + * @returns The configured logger instance + */ +export function getLogger(): pino.Logger { + if (_logger === null) { + _logger = createLogger(loggerConfig) + } + return _logger +} + +/** + * Request logging middleware + * + * This middleware: + * 1. Generates a unique request ID for tracing + * 2. Logs incoming request details + * 3. Measures request processing time + * 4. Logs response details after processing + * 5. Adds the request ID to response headers + * + * @returns Hono middleware handler function + */ +export function requestLogger() { + return async (c: AppContext, next: () => Promise): Promise => { + // Use x-request-id header if provided, otherwise generate a unique request ID + const requestId = c.req.header('x-request-id') || crypto.randomUUID() + + // Store request ID and logger in context + c.set('requestId', requestId) + const logger = getLogger().child({ requestId }) + c.set('logger', logger) + + // Capture request start time + const start = Date.now() + + // Log incoming request + logger.info({ + event: 'request_received', + method: c.req.method, + path: c.req.path, + query: c.req.query(), + userAgent: c.req.header('user-agent'), + ip: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'), + contentType: c.req.header('content-type'), + }) + + // Process the request + await next() + + // Calculate request duration + const duration = Date.now() - start + + // Log response + const responseStatus = c.res.status + const responseLog: Record = { + event: 'request_completed', + method: c.req.method, + path: c.req.path, + status: responseStatus, + duration, + } + + // Log at appropriate level based on status + if (responseStatus >= 500) { + logger.error(responseLog) + } else if (responseStatus >= 400) { + logger.warn(responseLog) + } else { + logger.info(responseLog) + } + + // Add request ID to response headers + c.header('X-Request-ID', requestId) + } +} + +// Export the logger for direct use +export { getLogger as logger } diff --git a/src/middleware/not-found.ts b/src/middleware/not-found.ts new file mode 100644 index 0000000..411ec84 --- /dev/null +++ b/src/middleware/not-found.ts @@ -0,0 +1,44 @@ +/** + * Not Found Handler Middleware + * + * This middleware provides a consistent 404 response for unmatched routes. + * It should be registered last in the middleware chain to catch all + * requests that don't match any defined routes. + */ + +import type { NotFoundHandler } from 'hono' +import type { AppContext } from '../types/index.ts' + +/** + * Create a standardized not-found response + * + * @param c - The Hono context + * @returns A JSON response with 404 status + */ +export const notFoundHandler: NotFoundHandler = (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + + const logger = c.get('logger') + if (logger) { + logger.warn({ + event: 'route_not_found', + method: c.req.method, + path: c.req.path, + requestId, + }) + } + + return c.json( + { + error: { + code: 'NOT_FOUND', + message: `Route ${c.req.method} ${c.req.path} not found`, + }, + meta: { + timestamp: new Date().toISOString(), + requestId, + }, + }, + 404 + ) +} diff --git a/src/routes/api.ts b/src/routes/api.ts new file mode 100644 index 0000000..7966a47 --- /dev/null +++ b/src/routes/api.ts @@ -0,0 +1,77 @@ +/** + * API Routes Placeholder + * + * This module serves as a placeholder for future API routes. + * Add your API endpoints here following the patterns established + * in the health check routes. + * + * Example route structure: + * - GET /api/v1/users - List users + * - POST /api/v1/users - Create user + * - GET /api/v1/users/:id - Get user by ID + */ + +import { Hono } from 'hono' +import type { AppEnv, AppContext } from '../types/index.ts' + +/** + * API router placeholder + * + * This router is mounted at /api and serves as the entry point + * for versioned API routes. Add your routes here. + */ +export const apiRouter = new Hono() + +/** + * Root API endpoint + * + * GET /api + * + * Returns API information and available versions + */ +apiRouter.get('/', (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + + return c.json({ + name: 'Hono API', + version: process.env.APP_VERSION || '1.0.0', + description: 'Production-ready API built with Hono.js and Bun', + endpoints: { + health: '/api/health', + v1: '/api/v1', + }, + meta: { + requestId, + timestamp: new Date().toISOString(), + }, + }) +}) + +/** + * API info endpoint + * + * GET /api/info + * + * Returns detailed API information including available routes + */ +apiRouter.get('/info', (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + + return c.json({ + name: 'Hono API', + version: process.env.APP_VERSION || '1.0.0', + environment: process.env.NODE_ENV || 'development', + documentation: '/api/docs', + meta: { + requestId, + timestamp: new Date().toISOString(), + }, + }) +}) + +// TODO: Add your API routes here +// Example: +// apiRouter.get('/users', userRoutes) +// apiRouter.get('/posts', postRoutes) + +export default apiRouter diff --git a/src/routes/health.ts b/src/routes/health.ts new file mode 100644 index 0000000..e1c7f6f --- /dev/null +++ b/src/routes/health.ts @@ -0,0 +1,165 @@ +/** + * Health Check Routes + * + * This module provides health check endpoints for monitoring + * application status and dependencies. It includes both basic + * and detailed health checks with component status reporting. + */ + +import { Hono } from 'hono' +import type { AppEnv, AppContext } from '../types/index.ts' + +/** + * Health check router + * Provides endpoints for monitoring application health + */ +export const healthRouter = new Hono() + +/** + * Basic health check endpoint + * + * Returns the basic application status without checking dependencies. + * Use this for load balancer health checks. + * + * GET /health + * + * Response: + * { + * "status": "ok", + * "timestamp": "2024-01-15T10:30:00Z", + * "uptime": 1234.56, + * "version": "1.0.0" + * } + */ +healthRouter.get('/', (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + + return c.json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: process.env.APP_VERSION || '1.0.0', + meta: { + requestId, + }, + }) +}) + +/** + * Detailed health check endpoint + * + * Checks the status of all application dependencies. + * Returns 503 if any critical component is unhealthy. + * + * GET /health/detailed + * + * Response: + * { + * "status": "healthy|degraded|unhealthy", + * "timestamp": "2024-01-15T10:30:00Z", + * "uptime": 1234.56, + * "checks": { + * "database": { "status": "ok", "latency": 5 }, + * "redis": { "status": "ok", "latency": 2 } + * } + * } + */ +healthRouter.get('/detailed', async (c: AppContext) => { + const requestId = c.get('requestId') || 'unknown' + const checks: Record = {} + + // Check database connectivity (placeholder) + try { + const start = Date.now() + // await db.execute('SELECT 1') + checks.database = { + status: 'ok', + latency: Date.now() - start, + } + } catch (error) { + checks.database = { + status: 'error', + error: error instanceof Error ? error.message : 'Unknown error', + } + } + + // Check Redis connectivity (placeholder) + try { + const start = Date.now() + // await redis.ping() + checks.redis = { + status: 'ok', + latency: Date.now() - start, + } + } catch (error) { + checks.redis = { + status: 'error', + error: error instanceof Error ? error.message : 'Unknown error', + } + } + + // Determine overall health status + const allHealthy = Object.values(checks).every((check) => check.status === 'ok') + const anyDegraded = Object.values(checks).some((check) => check.status === 'degraded') + + const overallStatus = allHealthy ? 'healthy' : anyDegraded ? 'degraded' : 'unhealthy' + + return c.json( + { + status: overallStatus, + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: process.env.APP_VERSION || '1.0.0', + checks, + meta: { + requestId, + }, + }, + allHealthy ? 200 : 503 + ) +}) + +/** + * Liveness probe endpoint + * + * Simple endpoint that returns 200 if the application is running. + * Kubernetes uses this to determine if the container should be restarted. + * + * GET /health/live + */ +healthRouter.get('/live', (c: AppContext) => { + return c.json({ + status: 'ok', + timestamp: new Date().toISOString(), + }) +}) + +/** + * Readiness probe endpoint + * + * Returns 200 if the application is ready to accept traffic. + * Kubernetes uses this to determine if the container should receive traffic. + * + * GET /health/ready + */ +healthRouter.get('/ready', async (c: AppContext) => { + // Check if all dependencies are ready + const isReady = true // Implement actual readiness checks + + if (isReady) { + return c.json({ + status: 'ready', + timestamp: new Date().toISOString(), + }) + } + + return c.json( + { + status: 'not_ready', + timestamp: new Date().toISOString(), + }, + 503 + ) +}) + +export default healthRouter diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..384f48c --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,49 @@ +/** + * Route Aggregator + * + * This module aggregates all route modules and exports a single + * router that can be mounted on the main application. Routes are + * organized by domain and mounted under appropriate path prefixes. + */ + +import { Hono } from 'hono' +import type { AppEnv } from '../types/index.ts' +import healthRouter from './health.ts' +import apiRouter from './api.ts' + +/** + * Main routes router + * + * This router combines all application routes under their + * respective path prefixes. The route hierarchy is: + * - /health - Health check endpoints + * - /api - API endpoints + */ +export const routes = new Hono() + +/** + * Mount health check routes at /health + * + * Provides endpoints for: + * - GET /health - Basic health check + * - GET /health/detailed - Detailed health with dependencies + * - GET /health/live - Liveness probe + * - GET /health/ready - Readiness probe + */ +routes.route('/health', healthRouter) + +/** + * Mount API routes at /api + * + * Provides endpoints for: + * - GET /api - API info + * - GET /api/info - Detailed API info + */ +routes.route('/api', apiRouter) + +// TODO: Add more route modules here +// Example: +// routes.route('/v1/users', userRoutes) +// routes.route('/v1/posts', postRoutes) + +export default routes diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..6f7be7d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,167 @@ +/** + * Common Type Definitions + * + * This module exports all common type definitions used throughout + * the application for type safety and consistency. + */ + +import type { Context } from 'hono' +import type pino from 'pino' + +/** + * Application environment type + * Defines the variables that can be set on the context + */ +export type AppEnv = { + Variables: { + requestId: string + logger: pino.Logger + } +} + +/** + * Application context type with custom bindings + * Extends the base Hono Context with application-specific data + */ +export type AppContext = Context + +/** + * Standard API response structure + * + * @typeParam T - The type of data contained in the response + */ +export type ApiResponse = { + /** The main response data */ + data: T + /** Response metadata */ + meta: { + /** ISO timestamp of when the response was generated */ + timestamp: string + /** Unique request identifier for tracing */ + requestId?: string + } +} + +/** + * Error response structure + * + * Provides consistent error formatting across all API endpoints + */ +export type ErrorResponse = { + /** Error details */ + error: { + /** Machine-readable error code */ + code: string + /** Human-readable error message */ + message: string + /** Detailed error information (optional) */ + details?: unknown + } + /** Response metadata */ + meta: { + /** ISO timestamp of when the error occurred */ + timestamp: string + /** Unique request identifier for tracing */ + requestId?: string + } +} + +/** + * Pagination parameters + * + * Common pagination interface for list endpoints + */ +export type PaginationParams = { + /** Page number (1-indexed) */ + page?: number + /** Number of items per page */ + limit?: number +} + +/** + * Paginated response structure + * + * @typeParam T - The type of items in the paginated list + */ +export type PaginatedResponse = { + /** Array of items for the current page */ + items: T[] + /** Pagination metadata */ + meta: { + /** Current page number */ + page: number + /** Number of items per page */ + limit: number + /** Total number of items across all pages */ + total: number + /** Total number of pages */ + totalPages: number + /** Whether there are more pages after current */ + hasMore: boolean + } +} + +/** + * HTTP method type alias + * + * Union type of all supported HTTP methods + */ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD' + +/** + * Route handler function type + * + * Generic type for route handler functions + */ +export type RouteHandler = ( + c: AppContext, + next: () => Promise +) => Promise | Response | void + +/** + * Service result type + * + * Wrapper type for service layer return values + * @typeParam T - The type of successful result data + */ +export type ServiceResult = + | { success: true; data: T } + | { success: false; error: string } + +/** + * Health check status type + */ +export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' + +/** + * Health check result interface + */ +export type HealthCheckResult = { + /** Overall health status */ + status: HealthStatus + /** ISO timestamp of the check */ + timestamp: string + /** Application uptime in seconds */ + uptime: number + /** Individual component health checks */ + checks: { + /** Database connectivity status */ + database?: { + status: HealthStatus + latency?: number + error?: string + } + /** Redis connectivity status */ + redis?: { + status: HealthStatus + latency?: number + error?: string + } + /** Custom additional checks */ + [key: string]: { + status: HealthStatus + latency?: number + error?: string + } | undefined + } +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..7e9bf59 --- /dev/null +++ b/src/utils/logger.ts @@ -0,0 +1,93 @@ +/** + * Logger Utility + * + * This module provides a configured Pino logger instance with: + * - Environment-based log level configuration + * - Pretty printing for development + * - JSON formatting for production + */ + +import pino from 'pino' + +/** + * Get log level from environment or default to 'info' + */ +function getLogLevel(): pino.LevelWithSilent { + const envLevel = process.env.LOG_LEVEL?.toLowerCase() + + switch (envLevel) { + case 'trace': + return 'trace' + case 'debug': + return 'debug' + case 'info': + return 'info' + case 'warn': + case 'warning': + return 'warn' + case 'error': + return 'error' + case 'fatal': + return 'fatal' + case 'silent': + return 'silent' + default: + return 'info' + } +} + +/** + * Check if running in development mode + */ +function isDevelopment(): boolean { + return process.env.NODE_ENV !== 'production' +} + +/** + * Create and configure the logger instance + */ +const logger = pino({ + level: getLogLevel(), + + // Use pretty printing in development, JSON in production + transport: isDevelopment() + ? { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'SYS:standard', + ignore: 'pid,hostname', + }, + } + : undefined, + + // Add standard metadata + base: { + env: process.env.NODE_ENV || 'development', + version: process.env.APP_VERSION || '1.0.0', + }, + + // Formatter options + formatters: { + level: (label: string) => { + return { level: label } + }, + }, + + // Timestamp format + timestamp: pino.stdTimeFunctions.isoTime, +}) + +export default logger + +/** + * Get a child logger with additional context + * + * @param bindings - Additional context to add to each log entry + * @returns Child logger instance + */ +export function getChildLogger( + bindings: Record +): pino.Logger { + return logger.child(bindings) +} diff --git a/src/utils/response.ts b/src/utils/response.ts deleted file mode 100644 index 72aea6c..0000000 --- a/src/utils/response.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Context } from "hono"; - -export interface JSendResponse { - status: "success" | "fail" | "error"; - data?: T; - message?: string; - meta?: { - total?: number; - page?: number; - limit?: number; - offset?: number; - }; -} - -export function success( - c: Context, - data: T, - meta?: JSendResponse["meta"], -) { - const response: JSendResponse = { - status: "success", - data, - }; - - if (meta) { - response.meta = meta; - } - - return c.json(response, 200); -} - -export function created(c: Context, data: T) { - const response: JSendResponse = { - status: "success", - data, - }; - - return c.json(response, 201); -} - -export function noContent(c: Context) { - return c.json({ status: "success" }, 204); -} - -export function badRequest( - c: Context, - message: string, - errors?: Record, -) { - const response: JSendResponse = { - status: "fail", - message, - }; - - if (errors) { - (response as JSendResponse & { errors: Record }).errors = - errors; - } - - return c.json(response, 400); -} - -export function unauthorized(c: Context, message: string) { - const response: JSendResponse = { - status: "fail", - message, - }; - - return c.json(response, 401); -} - -export function forbidden(c: Context, message: string) { - const response: JSendResponse = { - status: "fail", - message, - }; - - return c.json(response, 403); -} - -export function notFound(c: Context, message: string) { - const response: JSendResponse = { - status: "fail", - message, - }; - - return c.json(response, 404); -} - -export function conflict(c: Context, message: string) { - const response: JSendResponse = { - status: "fail", - message, - }; - - return c.json(response, 409); -} - -export function internalError(c: Context, message: string) { - const response: JSendResponse = { - status: "error", - message, - }; - - return c.json(response, 500); -} - -export function errorResponse(c: Context, status: number, message: string) { - const response: JSendResponse = { - status: status >= 500 ? "error" : "fail", - message, - }; - - return c.json(response, status); -} diff --git a/test/health.test.ts b/test/health.test.ts new file mode 100644 index 0000000..fbee2d3 --- /dev/null +++ b/test/health.test.ts @@ -0,0 +1,134 @@ +/** + * Health Check Endpoint Tests + * + * This module tests the health check endpoints including: + * - Basic health check (GET /health) + * - Detailed health check (GET /health/detailed) + * - Liveness probe (GET /health/live) + * - Readiness probe (GET /health/ready) + */ + +import { describe, it, expect, beforeAll } from 'bun:test' +import { Hono } from 'hono' +import { healthRouter } from '../src/routes/health.ts' +import { requestLogger } from '../src/middleware/logger.ts' +import type { AppContext } from '../src/types/index.ts' + +/** + * Create a test app with health routes + */ +function createTestApp(): Hono { + const app = new Hono() + app.use(requestLogger()) + app.route('/health', healthRouter) + return app +} + +describe('Health Endpoints', () => { + let app: Hono + + beforeAll(() => { + app = createTestApp() + }) + + describe('GET /health', () => { + it('should return health status', async () => { + const response = await app.request('/health', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(200) + + const body = await response.json() + expect(body.status).toBe('ok') + expect(body.timestamp).toBeDefined() + expect(typeof body.uptime).toBe('number') + expect(body.version).toBeDefined() + expect(body.meta?.requestId).toBe('test-request-id') + }) + + it('should include request ID in response', async () => { + const response = await app.request('/health', { + method: 'GET', + headers: { + 'x-request-id': 'custom-request-id', + }, + }) + + const body = await response.json() + expect(body.meta?.requestId).toBe('custom-request-id') + }) + }) + + describe('GET /health/detailed', () => { + it('should return detailed health status', async () => { + const response = await app.request('/health/detailed', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(200) + + const body = await response.json() + expect(body.status).toBeDefined() + expect(body.checks).toBeDefined() + expect(body.checks.database).toBeDefined() + expect(body.checks.redis).toBeDefined() + }) + + it('should include all required fields', async () => { + const response = await app.request('/health/detailed', { + method: 'GET', + }) + + const body = await response.json() + expect(body.timestamp).toBeDefined() + expect(typeof body.uptime).toBe('number') + expect(body.version).toBeDefined() + }) + }) + + describe('GET /health/live', () => { + it('should return liveness status', async () => { + const response = await app.request('/health/live', { + method: 'GET', + }) + + expect(response.status).toBe(200) + + const body = await response.json() + expect(body.status).toBe('ok') + expect(body.timestamp).toBeDefined() + }) + }) + + describe('GET /health/ready', () => { + it('should return readiness status when ready', async () => { + const response = await app.request('/health/ready', { + method: 'GET', + }) + + expect(response.status).toBe(200) + + const body = await response.json() + expect(body.status).toBe('ready') + expect(body.timestamp).toBeDefined() + }) + + it('should return 503 when not ready', async () => { + // Simulate not ready state by checking internal state + const response = await app.request('/health/ready', { + method: 'GET', + }) + + // The current implementation always returns ready + // This test would need modification if readiness logic changes + expect(response.status).toBe(200) + }) + }) +}) diff --git a/test/middleware/error-handler.test.ts b/test/middleware/error-handler.test.ts new file mode 100644 index 0000000..6655c35 --- /dev/null +++ b/test/middleware/error-handler.test.ts @@ -0,0 +1,224 @@ +/** + * Error Handler Middleware Tests + * + * This module tests the error handler middleware including: + * - Zod validation error handling + * - HTTP exception handling + * - Generic error handling + * - Not found error handling + */ + +import { describe, it, expect, beforeAll } from 'bun:test' +import { Hono } from 'hono' +import { HTTPException } from 'hono/http-exception' +import { ZodError } from 'zod' +import { requestLogger } from '../../src/middleware/logger.ts' +import { errorHandler, notFoundHandler } from '../../src/middleware/error-handler.ts' +import type { AppContext } from '../../src/types/index.ts' + +/** + * Create a test app with error handlers + */ +function createTestApp(): Hono { + const app = new Hono() + + // Request logger middleware (sets requestId from x-request-id header) + app.use(requestLogger()) + + // Test routes that throw errors + app.get('/zod-error', () => { + throw new ZodError([ + { + path: ['email'], + message: 'Invalid email format', + code: 'invalid_string', + }, + ]) + }) + + app.get('/http-error', () => { + throw new HTTPException(400, { message: 'Bad Request' }) + }) + + app.get('/generic-error', () => { + throw new Error('Something went wrong') + }) + + // Not found route (handled by notFoundHandler) + app.notFound(notFoundHandler) + + // Error handler + app.onError(errorHandler) + + return app +} + +describe('Error Handler Middleware', () => { + let app: Hono + + beforeAll(() => { + app = createTestApp() + }) + + describe('Zod Validation Errors', () => { + it('should handle Zod validation errors', async () => { + const response = await app.request('/zod-error', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(400) + + const body = await response.json() + expect(body.error.code).toBe('VALIDATION_ERROR') + expect(body.error.message).toBe('Invalid request data') + expect(body.error.details).toBeDefined() + expect(Array.isArray(body.error.details)).toBe(true) + expect(body.error.details[0].field).toBe('email') + }) + + it('should include request ID in error response', async () => { + const response = await app.request('/zod-error', { + method: 'GET', + headers: { + 'x-request-id': 'custom-id', + }, + }) + + const body = await response.json() + expect(body.meta.requestId).toBe('custom-id') + }) + }) + + describe('HTTP Exceptions', () => { + it('should handle HTTP exceptions with status code', async () => { + const response = await app.request('/http-error', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(400) + + const body = await response.json() + expect(body.error.code).toBe('400') + expect(body.error.message).toBe('Bad Request') + }) + + it('should handle different HTTP status codes', async () => { + const testApp = new Hono() + + testApp.get('/401', () => { + throw new HTTPException(401, { message: 'Unauthorized' }) + }) + + testApp.get('/403', () => { + throw new HTTPException(403, { message: 'Forbidden' }) + }) + + testApp.get('/404', () => { + throw new HTTPException(404, { message: 'Not Found' }) + }) + + testApp.get('/500', () => { + throw new HTTPException(500, { message: 'Internal Server Error' }) + }) + + testApp.onError(errorHandler) + + const responses = await Promise.all([ + testApp.request('/401', { method: 'GET' }), + testApp.request('/403', { method: 'GET' }), + testApp.request('/404', { method: 'GET' }), + testApp.request('/500', { method: 'GET' }), + ]) + + expect(responses[0].status).toBe(401) + expect(responses[1].status).toBe(403) + expect(responses[2].status).toBe(404) + expect(responses[3].status).toBe(500) + }) + }) + + describe('Generic Errors', () => { + it('should handle generic errors', async () => { + const response = await app.request('/generic-error', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(500) + + const body = await response.json() + expect(body.error.code).toBe('INTERNAL_SERVER_ERROR') + }) + + it('should hide error details in production', async () => { + // Set production mode + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'production' + + const response = await app.request('/generic-error', { + method: 'GET', + }) + + const body = await response.json() + expect(body.error.message).toBe('An unexpected error occurred') + expect(body.error.details).toBeUndefined() + + // Restore original env + process.env.NODE_ENV = originalEnv + }) + + it('should show error details in development', async () => { + // Set development mode + const originalEnv = process.env.NODE_ENV + process.env.NODE_ENV = 'development' + + const response = await app.request('/generic-error', { + method: 'GET', + }) + + const body = await response.json() + expect(body.error.message).toBe('Something went wrong') + expect(body.error.details).toBeDefined() + + // Restore original env + process.env.NODE_ENV = originalEnv + }) + }) + + describe('Not Found Handler', () => { + it('should handle 404 errors', async () => { + const response = await app.request('/nonexistent', { + method: 'GET', + headers: { + 'x-request-id': 'test-request-id', + }, + }) + + expect(response.status).toBe(404) + + const body = await response.json() + expect(body.error.code).toBe('NOT_FOUND') + expect(body.error.message).toContain('/nonexistent') + }) + + it('should include request ID in 404 response', async () => { + const response = await app.request('/missing-route', { + method: 'GET', + headers: { + 'x-request-id': 'custom-id', + }, + }) + + const body = await response.json() + expect(body.meta.requestId).toBe('custom-id') + }) + }) +}) diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..8e12768 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,156 @@ +/** + * Test Setup for Bun Test Runner + * + * This module provides: + * - Test environment configuration + * - Common test utilities and helpers + * - Mock cleanup between tests + */ + +// Set test environment variables before importing anything else +process.env.NODE_ENV = 'test' +process.env.LOG_LEVEL = 'error' // Suppress logs during tests +process.env.APP_VERSION = '1.0.0-test' + +/** + * Common assertion utilities + */ +export const testUtils = { + /** + * Assert that a value is truthy + */ + assertTruthy: (value: unknown, message = 'Expected truthy value'): void => { + if (!value) { + throw new Error(message) + } + }, + + /** + * Assert that two values are equal + */ + assertEquals: (actual: T, expected: T, message?: string): void => { + if (actual !== expected) { + throw new Error( + message || `Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}` + ) + } + }, + + /** + * Assert that a value is undefined + */ + assertUndefined: (value: unknown, message = 'Expected undefined'): void => { + if (value !== undefined) { + throw new Error(message) + } + }, + + /** + * Assert that a value is null + */ + assertNull: (value: unknown, message = 'Expected null'): void => { + if (value !== null) { + throw new Error(message) + } + }, +} + +/** + * Create a mock response object + */ +export function createMockResponse(): { + status: number + headers: Map + body: unknown + json(body: unknown): this + text(body: string): this + setHeader(name: string, value: string): this + status(status: number): this +} { + let responseStatus = 200 + const responseHeaders = new Map() + let responseBody: unknown = null + + return { + get status() { + return responseStatus + }, + get headers() { + return responseHeaders + }, + get body() { + return responseBody + }, + json(body: unknown) { + responseBody = body + responseHeaders.set('Content-Type', 'application/json') + return this + }, + text(body: string) { + responseBody = body + responseHeaders.set('Content-Type', 'text/plain') + return this + }, + setHeader(name: string, value: string) { + responseHeaders.set(name.toLowerCase(), value) + return this + }, + status(status: number) { + responseStatus = status + return this + }, + } +} + +/** + * Create a mock request object + */ +export function createMockRequest(options: { + method?: string + path?: string + headers?: Record + body?: unknown +}): { + method: string + path: string + headers: Map + body: unknown + param(name: string): string | undefined + query(name: string): string | undefined +} { + const requestMethod = options.method || 'GET' + const requestPath = options.path || '/' + const requestHeaders = new Map(Object.entries(options.headers || {})) + const requestBody = options.body + + return { + get method() { + return requestMethod + }, + get path() { + return requestPath + }, + get headers() { + return requestHeaders + }, + get body() { + return requestBody + }, + param(name: string): string | undefined { + // Simple param extraction for testing + const match = requestPath.match(new RegExp(`/${name}/([^/]+)`)) + return match?.[1] + }, + query(name: string): string | undefined { + // Simple query extraction for testing + const url = new URL(requestPath, 'http://localhost') + return url.searchParams.get(name) || undefined + }, + } +} + +export default { + testUtils, + createMockResponse, + createMockRequest, +} diff --git a/tests/integration/setup.test.ts b/tests/integration/setup.test.ts deleted file mode 100644 index 219ba4f..0000000 --- a/tests/integration/setup.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -/** - * Integration Test Setup Utilities - * - * Provides database connection, migration, seeding, and cleanup utilities - * for integration tests with real PostgreSQL database. - */ - -import { config } from "dotenv"; -import postgres from "postgres"; -import { drizzle } from "drizzle-orm/postgres-js"; -import { migrate } from "drizzle-orm/postgres-js/migrator"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; - -// Load test environment variables -config({ path: ".env.test" }); - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Database configuration from environment -export const testDbConfig = { - host: process.env.DATABASE_HOST || "localhost", - port: parseInt(process.env.DATABASE_PORT || "5432", 10), - user: process.env.DATABASE_USER || "test_user", - password: process.env.DATABASE_PASSWORD || "test_password", - dbname: process.env.DATABASE_NAME || "test_db", - max_conns: parseInt(process.env.DATABASE_MAX_CONNS || "5", 10), - min_conns: parseInt(process.env.DATABASE_MIN_CONNS || "1", 10), - max_conn_lifetime: process.env.DATABASE_MAX_CONN_LIFETIME || "5m", - max_conn_idletime: process.env.DATABASE_MAX_CONN_IDLETIME || "1m", - health_check_period: process.env.DATABASE_HEALTH_CHECK_PERIOD || "30s", -}; - -/** - * Create a test database connection - */ -export async function createTestConnection(): Promise { - const connectionString = `postgres://${testDbConfig.user}:${testDbConfig.password}@${testDbConfig.host}:${testDbConfig.port}/${testDbConfig.dbname}`; - - return postgres(connectionString, { - max: testDbConfig.max_conns, - min: testDbConfig.min_conns, - idle_timeout: parseDuration(testDbConfig.max_conn_idletime), - connect_timeout: parseDuration(testDbConfig.max_conn_lifetime), - }); -} - -/** - * Run migrations on test database - */ -export async function runMigrations(): Promise { - const sql = await createTestConnection(); - const db = drizzle(sql); - - const migrationsFolder = path.resolve(__dirname, "../../drizzle/migrations"); - - if (!fs.existsSync(migrationsFolder)) { - throw new Error(`Migrations folder not found: ${migrationsFolder}`); - } - - await migrate(db, { migrationsFolder }); - await sql.end(); -} - -/** - * Clean up all data from test database - * WARNING: This will delete ALL data in the test database - */ -export async function cleanupDatabase(): Promise { - const sql = await createTestConnection(); - - // Disable foreign key checks temporarily for clean truncation - await sql`SET CONSTRAINTS ALL DEFERRED`; - - // Truncate all tables in correct order (respecting foreign key dependencies) - await sql`TRUNCATE TABLE tasks CASCADE`; - await sql`TRUNCATE TABLE users CASCADE`; - - await sql.end(); -} - -/** - * Seed test data for User domain - */ -export async function seedTestUsers(): Promise< - { id: string; email: string; password: string }[] -> { - const sql = await createTestConnection(); - - const testUsers = [ - { - email: "test1@example.com", - password: - "$argon2id$v=19$m=65536,t=3,p=1$test salt test salt test salt test salt$test hash test hash test hash test hash test hash test hash", - full_name: "Test User 1", - phone: "123-456-7890", - is_active: true, - }, - { - email: "test2@example.com", - password: - "$argon2id$v=19$m=65536,t=3,p=1$another salt another salt another salt another salt$another hash another hash another hash", - full_name: "Test User 2", - phone: "098-765-4321", - is_active: true, - }, - { - email: "inactive@example.com", - password: - "$argon2id$v=19$m=65536,t=3,p=1$inactive salt inactive salt inactive salt$inactive hash inactive hash inactive hash", - full_name: "Inactive User", - phone: "555-555-5555", - is_active: false, - }, - ]; - - const insertedUsers = await sql` - INSERT INTO users (email, password, full_name, phone, is_active) - VALUES ${sql(testUsers.map((u) => [u.email, u.password, u.full_name, u.phone, u.is_active]))} - RETURNING id, email, password - `; - - await sql.end(); - return insertedUsers.map((u) => ({ - id: u.id, - email: u.email, - password: u.password, - })); -} - -/** - * Seed test data for Task domain - */ -export async function seedTestTasks( - userId: string, -): Promise<{ id: string; title: string }[]> { - const sql = await createTestConnection(); - - const testTasks = [ - { - user_id: userId, - title: "Test Task 1", - description: "First test task", - status: "pending", - priority: "high", - }, - { - user_id: userId, - title: "Test Task 2", - description: "Second test task", - status: "in_progress", - priority: "medium", - }, - { - user_id: userId, - title: "Test Task 3", - description: "Third test task", - status: "completed", - priority: "low", - }, - { - user_id: userId, - title: "Urgent Task", - description: "Urgent task to test priority", - status: "pending", - priority: "urgent", - }, - { - user_id: userId, - title: "Task without description", - description: null, - status: "pending", - priority: "medium", - }, - ]; - - const insertedTasks = await sql` - INSERT INTO tasks (user_id, title, description, status, priority) - VALUES ${sql(testTasks.map((t) => [t.user_id, t.title, t.description, t.status, t.priority]))} - RETURNING id, title - `; - - await sql.end(); - return insertedTasks.map((t) => ({ id: t.id, title: t.title })); -} - -/** - * Delete test user by ID (and cascade delete associated tasks) - */ -export async function deleteTestUser(userId: string): Promise { - const sql = await createTestConnection(); - await sql`DELETE FROM users WHERE id = ${userId}`; - await sql.end(); -} - -/** - * Delete test task by ID - */ -export async function deleteTestTask(taskId: string): Promise { - const sql = await createTestConnection(); - await sql`DELETE FROM tasks WHERE id = ${taskId}`; - await sql.end(); -} - -/** - * Check if test database is healthy - */ -export async function isDatabaseHealthy(): Promise { - try { - const sql = await createTestConnection(); - const result = await sql`SELECT 1 as healthy`; - await sql.end(); - return result[0]?.healthy === 1; - } catch { - return false; - } -} - -/** - * Wait for database to be ready with retries - */ -export async function waitForDatabase( - maxRetries: number = 30, - intervalMs: number = 1000, -): Promise { - for (let i = 0; i < maxRetries; i++) { - if (await isDatabaseHealthy()) { - return true; - } - await new Promise((resolve) => setTimeout(resolve, intervalMs)); - } - return false; -} - -/** - * Parse duration string to milliseconds - */ -function parseDuration(duration: string): number { - const match = duration.match(/^(\d+)([smh])$/); - if (!match) { - return 60000; // default 1 minute - } - - const value = parseInt(match[1], 10); - const unit = match[2]; - - switch (unit) { - case "s": - return value * 1000; - case "m": - return value * 60 * 1000; - case "h": - return value * 60 * 60 * 1000; - default: - return 60000; - } -} - -// Re-export for convenience -export { sql } from "drizzle-orm"; diff --git a/tests/mocks/mock-repository.test.ts b/tests/mocks/mock-repository.test.ts deleted file mode 100644 index 8e6d44c..0000000 --- a/tests/mocks/mock-repository.test.ts +++ /dev/null @@ -1,678 +0,0 @@ -import type { - Task, - CreateTask, - UpdateTask, -} from "../../src/domain/task/entity/task.js"; -import type { ITaskRepository } from "../../src/domain/task/repository/task.js"; -import type { - User, - CreateUser, - UpdateUser, -} from "../../src/domain/user/entity/user.js"; -import type { IUserRepository } from "../../src/domain/user/repository/user.js"; -import type { Logger } from "../../src/infrastructure/logger/logger.js"; -import { describe, test, expect } from "bun:test"; - -// ============ Mock Logger ============ - -export class MockLogger implements Logger { - private logs: Array<{ - level: string; - message: string; - meta?: Record; - }> = []; - - debug(message: string, meta?: Record): void { - this.logs.push({ level: "debug", message, meta }); - } - - info(message: string, meta?: Record): void { - this.logs.push({ level: "info", message, meta }); - } - - warn(message: string, meta?: Record): void { - this.logs.push({ level: "warn", message, meta }); - } - - error(message: string, meta?: Record): void { - this.logs.push({ level: "error", message, meta }); - } - - fatal(message: string, meta?: Record): void { - this.logs.push({ level: "fatal", message, meta }); - } - - child(bindings: Record): Logger { - const childLogger = new MockLogger(); - childLogger.logs = [...this.logs]; - return childLogger; - } - - getLogs(): Array<{ - level: string; - message: string; - meta?: Record; - }> { - return this.logs; - } - - clearLogs(): void { - this.logs = []; - } -} - -// ============ Mock Task Repository ============ - -export class MockTaskRepository implements ITaskRepository { - private tasks: Map = new Map(); - private logger: MockLogger; - - constructor(logger: MockLogger) { - this.logger = logger; - } - - setTasks(tasks: Task[]): void { - this.tasks.clear(); - for (const task of tasks) { - this.tasks.set(task.id, task); - } - } - - getAllTasks(): Task[] { - return Array.from(this.tasks.values()); - } - - async create(task: CreateTask): Promise { - const now = new Date(); - const newTask: Task = { - id: `task-${Date.now()}-${Math.random().toString(36).substring(7)}`, - userId: task.userId, - title: task.title, - description: task.description, - status: task.status || "pending", - priority: task.priority || "medium", - dueDate: task.dueDate, - completedAt: undefined, - createdAt: now, - updatedAt: now, - }; - this.tasks.set(newTask.id, newTask); - return newTask; - } - - async getByID(id: string): Promise { - return this.tasks.get(id) || null; - } - - async listByUser( - userId: string, - limit: number, - offset: number, - ): Promise<{ tasks: Task[]; total: number }> { - const userTasks = Array.from(this.tasks.values()).filter( - (t) => t.userId === userId, - ); - const total = userTasks.length; - const paginatedTasks = userTasks.slice(offset, offset + limit); - return { tasks: paginatedTasks, total }; - } - - async update( - id: string, - userId: string, - task: UpdateTask, - ): Promise { - const existing = this.tasks.get(id); - if (!existing || existing.userId !== userId) { - return null; - } - - const updated: Task = { - ...existing, - ...task, - updatedAt: new Date(), - completedAt: - task.status === "completed" ? new Date() : existing.completedAt, - }; - this.tasks.set(id, updated); - return updated; - } - - async delete(id: string, userId: string): Promise { - const existing = this.tasks.get(id); - if (existing && existing.userId === userId) { - this.tasks.delete(id); - } - } -} - -// ============ Mock User Repository ============ - -export class MockUserRepository implements IUserRepository { - private users: Map = new Map(); - private emailIndex: Map = new Map(); // email -> id - private logger: MockLogger; - - constructor(logger: MockLogger) { - this.logger = logger; - } - - setUsers(users: User[]): void { - this.users.clear(); - this.emailIndex.clear(); - for (const user of users) { - this.users.set(user.id, user); - this.emailIndex.set(user.email, user.id); - } - } - - getAllUsers(): User[] { - return Array.from(this.users.values()); - } - - async create(user: CreateUser): Promise { - const now = new Date(); - const newUser: User = { - id: `user-${Date.now()}-${Math.random().toString(36).substring(7)}`, - email: user.email, - password: user.password, - fullName: user.fullName, - phone: user.phone, - isActive: true, - createdAt: now, - updatedAt: now, - }; - this.users.set(newUser.id, newUser); - this.emailIndex.set(newUser.email, newUser.id); - return newUser; - } - - async getByID(id: string): Promise { - return this.users.get(id) || null; - } - - async getByEmail(email: string): Promise { - const id = this.emailIndex.get(email); - if (id) { - return this.users.get(id) || null; - } - return null; - } - - async update(id: string, user: UpdateUser): Promise { - const existing = this.users.get(id); - if (!existing) { - return null; - } - - const updated: User = { - ...existing, - fullName: user.fullName || existing.fullName, - phone: user.phone !== undefined ? user.phone : existing.phone, - updatedAt: new Date(), - }; - this.users.set(id, updated); - return updated; - } - - async delete(id: string): Promise { - const existing = this.users.get(id); - if (existing) { - this.emailIndex.delete(existing.email); - this.users.delete(id); - } - } - - async list( - limit: number, - offset: number, - ): Promise<{ users: User[]; total: number }> { - const allUsers = Array.from(this.users.values()); - const total = allUsers.length; - const paginatedUsers = allUsers.slice(offset, offset + limit); - return { users: paginatedUsers, total }; - } - - async verifyPassword(email: string, password: string): Promise { - const user = await this.getByEmail(email); - if (!user) return null; - return user; - } -} - -// ============ Mock Passworder ============ - -export class MockPassworder { - private hashStore: Map = new Map(); // password -> hash - - async hashPassword(password: string): Promise { - const hash = `mock_hash_${password}_${Date.now()}`; - this.hashStore.set(password, hash); - return hash; - } - - async verifyPassword(password: string, hash: string): Promise { - const storedHash = this.hashStore.get(password); - if (storedHash && storedHash.startsWith(`mock_hash_${password}_`)) { - return true; - } - return hash === `mock_hash_${password}_123`; - } -} - -// ============ Helper Functions ============ - -export function createMockTask(overrides: Partial = {}): Task { - const now = new Date(); - return { - id: `task-${Math.random().toString(36).substring(7)}`, - userId: "user-123", - title: "Test Task", - description: "Test description", - status: "pending", - priority: "medium", - dueDate: undefined, - completedAt: undefined, - createdAt: now, - updatedAt: now, - ...overrides, - }; -} - -export function createMockUser(overrides: Partial = {}): User { - const now = new Date(); - return { - id: `user-${Math.random().toString(36).substring(7)}`, - email: "test@example.com", - password: "hashed_password", - fullName: "Test User", - phone: undefined, - isActive: true, - createdAt: now, - updatedAt: now, - ...overrides, - }; -} - -export function createMockUserWithPassword( - overrides: Partial = {}, -): User { - return createMockUser({ - password: "mock_hash_password_123", - ...overrides, - }); -} - -// Pre-defined mock tasks -export const mockTasks: Task[] = [ - createMockTask({ - id: "task-pending-1", - status: "pending", - priority: "low", - title: "Pending Task 1", - }), - createMockTask({ - id: "task-pending-2", - status: "pending", - priority: "high", - title: "Pending Task 2", - }), - createMockTask({ - id: "task-in-progress-1", - status: "in_progress", - priority: "medium", - title: "In Progress Task", - }), - createMockTask({ - id: "task-completed-1", - status: "completed", - priority: "urgent", - title: "Completed Task", - completedAt: new Date(), - }), -]; - -// Pre-defined mock users -export const mockUsers: User[] = [ - createMockUserWithPassword({ - id: "user-1", - email: "user1@example.com", - fullName: "User One", - }), - createMockUserWithPassword({ - id: "user-2", - email: "user2@example.com", - fullName: "User Two", - }), -]; - -// ============ Mock Context ============ - -export interface MockContextOptions { - userId?: string | null; - param?: string; - query?: Record; - json?: Record; -} - -export class MockContext { - private options: MockContextOptions; - private responseData: unknown = null; - private responseStatus: number = 200; - private responseHeaders: Map = new Map(); - - constructor(options: MockContextOptions = {}) { - this.options = options; - } - - getRequestID(): string { - return "test-request-id"; - } - - getUserId(): string | null { - return this.options.userId || null; - } - - req = { - json: async () => this.options.json || {}, - param: (name: string) => this.options.param || "", - query: (name: string) => this.options.query?.[name] || "", - }; - - setStatus(status: number): void { - this.responseStatus = status; - } - - setData(data: unknown): void { - this.responseData = data; - } - - setHeader(name: string, value: string): void { - this.responseHeaders.set(name, value); - } - - getResponseStatus(): number { - return this.responseStatus; - } - - getResponseData(): unknown { - return this.responseData; - } - - getResponseHeaders(): Map { - return this.responseHeaders; - } -} - -// ============ Tests ============ - -describe("MockLogger", () => { - test("should log messages at different levels", () => { - const logger = new MockLogger(); - logger.debug("debug message"); - logger.info("info message"); - logger.warn("warn message"); - logger.error("error message"); - logger.fatal("fatal message"); - - const logs = logger.getLogs(); - expect(logs).toHaveLength(5); - expect(logs[0].level).toBe("debug"); - expect(logs[1].level).toBe("info"); - expect(logs[2].level).toBe("warn"); - expect(logs[3].level).toBe("error"); - expect(logs[4].level).toBe("fatal"); - }); - - test("should clear logs", () => { - const logger = new MockLogger(); - logger.info("test message"); - expect(logger.getLogs()).toHaveLength(1); - logger.clearLogs(); - expect(logger.getLogs()).toHaveLength(0); - }); -}); - -describe("MockTaskRepository", () => { - test("should create a task", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - const task = await repository.create({ - userId: "user-123", - title: "New Task", - description: "Task description", - }); - - expect(task).toBeDefined(); - expect(task.title).toBe("New Task"); - expect(task.userId).toBe("user-123"); - expect(task.status).toBe("pending"); - }); - - test("should get task by ID", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - const created = await repository.create({ - userId: "user-123", - title: "Test Task", - }); - - const found = await repository.getByID(created.id); - expect(found).not.toBeNull(); - expect(found?.title).toBe("Test Task"); - }); - - test("should return null for non-existent task", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - const result = await repository.getByID("non-existent-id"); - expect(result).toBeNull(); - }); - - test("should list tasks by user", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - await repository.create({ - userId: "user-1", - title: "Task 1", - }); - await repository.create({ - userId: "user-1", - title: "Task 2", - }); - await repository.create({ - userId: "user-2", - title: "Task 3", - }); - - const result = await repository.listByUser("user-1", 10, 0); - expect(result.tasks).toHaveLength(2); - expect(result.total).toBe(2); - }); - - test("should update task", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - const created = await repository.create({ - userId: "user-123", - title: "Original Title", - }); - - const updated = await repository.update(created.id, "user-123", { - title: "Updated Title", - status: "completed", - }); - - expect(updated).not.toBeNull(); - expect(updated?.title).toBe("Updated Title"); - expect(updated?.status).toBe("completed"); - }); - - test("should delete task", async () => { - const logger = new MockLogger(); - const repository = new MockTaskRepository(logger); - - const created = await repository.create({ - userId: "user-123", - title: "Task to Delete", - }); - - await repository.delete(created.id, "user-123"); - - const result = await repository.getByID(created.id); - expect(result).toBeNull(); - }); -}); - -describe("MockUserRepository", () => { - test("should create a user", async () => { - const logger = new MockLogger(); - const repository = new MockUserRepository(logger); - - const user = await repository.create({ - email: "test@example.com", - password: "hashed_password", - fullName: "Test User", - }); - - expect(user).toBeDefined(); - expect(user.email).toBe("test@example.com"); - expect(user.fullName).toBe("Test User"); - expect(user.isActive).toBe(true); - }); - - test("should get user by email", async () => { - const logger = new MockLogger(); - const repository = new MockUserRepository(logger); - - await repository.create({ - email: "findme@example.com", - password: "password", - fullName: "Find Me", - }); - - const found = await repository.getByEmail("findme@example.com"); - expect(found).not.toBeNull(); - expect(found?.fullName).toBe("Find Me"); - }); - - test("should update user", async () => { - const logger = new MockLogger(); - const repository = new MockUserRepository(logger); - - const created = await repository.create({ - email: "update@example.com", - password: "password", - fullName: "Original Name", - }); - - const updated = await repository.update(created.id, { - fullName: "Updated Name", - phone: "123-456-7890", - }); - - expect(updated).not.toBeNull(); - expect(updated?.fullName).toBe("Updated Name"); - expect(updated?.phone).toBe("123-456-7890"); - }); - - test("should delete user", async () => { - const logger = new MockLogger(); - const repository = new MockUserRepository(logger); - - const created = await repository.create({ - email: "delete@example.com", - password: "password", - fullName: "Delete Me", - }); - - await repository.delete(created.id); - - const result = await repository.getByID(created.id); - expect(result).toBeNull(); - }); - - test("should list users with pagination", async () => { - const logger = new MockLogger(); - const repository = new MockUserRepository(logger); - - for (let i = 0; i < 5; i++) { - await repository.create({ - email: `user${i}@example.com`, - password: "password", - fullName: `User ${i}`, - }); - } - - const result = await repository.list(2, 0); - expect(result.users).toHaveLength(2); - expect(result.total).toBe(5); - }); -}); - -describe("MockPassworder", () => { - test("should hash password", async () => { - const passworder = new MockPassworder(); - - const hash = await passworder.hashPassword("myPassword"); - - expect(hash).toBeDefined(); - expect(hash).toContain("myPassword"); - }); - - test("should verify correct password", async () => { - const passworder = new MockPassworder(); - - const hash = await passworder.hashPassword("myPassword"); - const isValid = await passworder.verifyPassword("myPassword", hash); - - expect(isValid).toBe(true); - }); - - test("should reject incorrect password", async () => { - const passworder = new MockPassworder(); - - const hash = await passworder.hashPassword("myPassword"); - const isValid = await passworder.verifyPassword("wrongPassword", hash); - - expect(isValid).toBe(false); - }); -}); - -describe("Helper Functions", () => { - test("createMockTask should create task with defaults", () => { - const task = createMockTask(); - - expect(task).toBeDefined(); - expect(task.title).toBe("Test Task"); - expect(task.status).toBe("pending"); - expect(task.priority).toBe("medium"); - expect(task.userId).toBe("user-123"); - }); - - test("createMockUser should create user with defaults", () => { - const user = createMockUser(); - - expect(user).toBeDefined(); - expect(user.email).toBe("test@example.com"); - expect(user.fullName).toBe("Test User"); - expect(user.isActive).toBe(true); - }); - - test("mockTasks should have predefined tasks", () => { - expect(mockTasks).toHaveLength(4); - expect(mockTasks[0].status).toBe("pending"); - expect(mockTasks[3].status).toBe("completed"); - }); - - test("mockUsers should have predefined users", () => { - expect(mockUsers).toHaveLength(2); - expect(mockUsers[0].email).toBe("user1@example.com"); - expect(mockUsers[1].email).toBe("user2@example.com"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 97dd944..a8ae517 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,22 +8,25 @@ "allowJs": true, "checkJs": false, "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "allowSyntheticDefaultImports": true, + "removeComments": true, + "noEmit": true, "isolatedModules": true, - "noEmit": false, - "declaration": true, - "declarationMap": true, - "sourceMap": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowImportingTsExtensions": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "types": ["bun-types"] }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist", "build"] }