From 71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 31 Dec 2025 16:26:53 +0800 Subject: [PATCH] feat(database): add database layer architecture (#410) - Add @esengine/database-drivers for MongoDB/Redis connection management - Add @esengine/database for Repository pattern with CRUD, pagination, soft delete - Refactor @esengine/transaction MongoStorage to use shared connection - Add comprehensive documentation in Chinese and English --- .changeset/database-layer-architecture.md | 48 +++ .../docs/en/modules/database-drivers/index.md | 136 +++++++ .../content/docs/en/modules/database/index.md | 217 +++++++++++ docs/src/content/docs/en/modules/index.md | 7 + .../docs/en/modules/transaction/storage.md | 38 +- .../docs/modules/database-drivers/index.md | 136 +++++++ .../docs/modules/database-drivers/mongo.md | 265 ++++++++++++++ .../docs/modules/database-drivers/redis.md | 228 ++++++++++++ .../content/docs/modules/database/index.md | 140 +++++++ .../content/docs/modules/database/query.md | 185 ++++++++++ .../docs/modules/database/repository.md | 244 +++++++++++++ .../src/content/docs/modules/database/user.md | 277 ++++++++++++++ docs/src/content/docs/modules/index.md | 7 + .../docs/modules/transaction/storage.md | 38 +- .../framework/database-drivers/module.json | 23 ++ .../framework/database-drivers/package.json | 48 +++ .../src/adapters/MongoCollectionAdapter.ts | 238 ++++++++++++ .../src/drivers/MongoConnection.ts | 343 ++++++++++++++++++ .../src/drivers/RedisConnection.ts | 300 +++++++++++++++ .../database-drivers/src/drivers/index.ts | 29 ++ .../framework/database-drivers/src/index.ts | 117 ++++++ .../src/interfaces/IMongoCollection.ts | 237 ++++++++++++ .../framework/database-drivers/src/tokens.ts | 56 +++ .../framework/database-drivers/src/types.ts | 338 +++++++++++++++++ .../framework/database-drivers/tsconfig.json | 10 + .../framework/database-drivers/tsup.config.ts | 11 + packages/framework/database/module.json | 23 ++ packages/framework/database/package.json | 37 ++ packages/framework/database/src/Repository.ts | 313 ++++++++++++++++ .../framework/database/src/UserRepository.ts | 335 +++++++++++++++++ packages/framework/database/src/index.ts | 152 ++++++++ packages/framework/database/src/password.ts | 189 ++++++++++ packages/framework/database/src/tokens.ts | 17 + packages/framework/database/src/types.ts | 333 +++++++++++++++++ packages/framework/database/tsconfig.json | 10 + packages/framework/database/tsup.config.ts | 11 + packages/framework/transaction/package.json | 2 +- packages/framework/transaction/src/index.ts | 4 +- .../transaction/src/storage/MongoStorage.ts | 224 ++++-------- .../transaction/src/storage/index.ts | 2 +- pnpm-lock.yaml | 44 ++- 41 files changed, 5226 insertions(+), 186 deletions(-) create mode 100644 .changeset/database-layer-architecture.md create mode 100644 docs/src/content/docs/en/modules/database-drivers/index.md create mode 100644 docs/src/content/docs/en/modules/database/index.md create mode 100644 docs/src/content/docs/modules/database-drivers/index.md create mode 100644 docs/src/content/docs/modules/database-drivers/mongo.md create mode 100644 docs/src/content/docs/modules/database-drivers/redis.md create mode 100644 docs/src/content/docs/modules/database/index.md create mode 100644 docs/src/content/docs/modules/database/query.md create mode 100644 docs/src/content/docs/modules/database/repository.md create mode 100644 docs/src/content/docs/modules/database/user.md create mode 100644 packages/framework/database-drivers/module.json create mode 100644 packages/framework/database-drivers/package.json create mode 100644 packages/framework/database-drivers/src/adapters/MongoCollectionAdapter.ts create mode 100644 packages/framework/database-drivers/src/drivers/MongoConnection.ts create mode 100644 packages/framework/database-drivers/src/drivers/RedisConnection.ts create mode 100644 packages/framework/database-drivers/src/drivers/index.ts create mode 100644 packages/framework/database-drivers/src/index.ts create mode 100644 packages/framework/database-drivers/src/interfaces/IMongoCollection.ts create mode 100644 packages/framework/database-drivers/src/tokens.ts create mode 100644 packages/framework/database-drivers/src/types.ts create mode 100644 packages/framework/database-drivers/tsconfig.json create mode 100644 packages/framework/database-drivers/tsup.config.ts create mode 100644 packages/framework/database/module.json create mode 100644 packages/framework/database/package.json create mode 100644 packages/framework/database/src/Repository.ts create mode 100644 packages/framework/database/src/UserRepository.ts create mode 100644 packages/framework/database/src/index.ts create mode 100644 packages/framework/database/src/password.ts create mode 100644 packages/framework/database/src/tokens.ts create mode 100644 packages/framework/database/src/types.ts create mode 100644 packages/framework/database/tsconfig.json create mode 100644 packages/framework/database/tsup.config.ts diff --git a/.changeset/database-layer-architecture.md b/.changeset/database-layer-architecture.md new file mode 100644 index 00000000..b0b26a17 --- /dev/null +++ b/.changeset/database-layer-architecture.md @@ -0,0 +1,48 @@ +--- +"@esengine/database-drivers": minor +"@esengine/database": minor +"@esengine/transaction": minor +--- + +feat: add database layer architecture + +Added new database packages with layered architecture: + +**@esengine/database-drivers (Layer 1)** +- MongoDB connection with pool management, auto-reconnect, events +- Redis connection with auto-reconnect, key prefix +- Type-safe `IMongoCollection` interface decoupled from mongodb types +- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`) + +**@esengine/database (Layer 2)** +- Generic `Repository` with CRUD, pagination, soft delete +- `UserRepository` with registration, authentication, role management +- Password hashing utilities using scrypt +- Query operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$regex` + +**@esengine/transaction** +- Refactored `MongoStorage` to use shared connection from `@esengine/database-drivers` +- Removed factory pattern in favor of shared connection (breaking change) +- Simplified API: `createMongoStorage(connection, options?)` + +Example usage: +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { UserRepository } from '@esengine/database' +import { createMongoStorage, TransactionManager } from '@esengine/transaction' + +// Create shared connection +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +// Use for database operations +const userRepo = new UserRepository(mongo) +await userRepo.register({ username: 'john', password: '123456' }) + +// Use for transactions (same connection) +const storage = createMongoStorage(mongo) +const txManager = new TransactionManager({ storage }) +``` diff --git a/docs/src/content/docs/en/modules/database-drivers/index.md b/docs/src/content/docs/en/modules/database-drivers/index.md new file mode 100644 index 00000000..70d32b92 --- /dev/null +++ b/docs/src/content/docs/en/modules/database-drivers/index.md @@ -0,0 +1,136 @@ +--- +title: "Database Drivers" +description: "MongoDB, Redis connection management and driver abstraction" +--- + +`@esengine/database-drivers` is ESEngine's database connection management layer, providing unified connection management for MongoDB, Redis, and more. + +## Features + +- **Connection Pool** - Automatic connection pool management +- **Auto Reconnect** - Automatic reconnection on disconnect +- **Event Notification** - Connection state change events +- **Type Decoupling** - Simplified interfaces, no dependency on native driver types +- **Shared Connections** - Single connection shared across modules + +## Installation + +```bash +npm install @esengine/database-drivers +``` + +**Peer Dependencies:** +```bash +npm install mongodb # For MongoDB support +npm install ioredis # For Redis support +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ @esengine/database-drivers (Layer 1) │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ MongoConnection │ │ RedisConnection │ │ +│ │ - Pool management │ │ - Auto-reconnect │ │ +│ │ - Auto-reconnect │ │ - Key prefix │ │ +│ │ - Event emitter │ │ - Event emitter │ │ +│ └──────────┬──────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌──────────▼──────────┐ │ +│ │ IMongoCollection │ ← Type-safe interface │ +│ │ (Adapter pattern) │ decoupled from mongodb types │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────┐ ┌───────────────────────┐ +│ @esengine/database │ │ @esengine/transaction │ +│ (Repository pattern) │ │ (Distributed tx) │ +└───────────────────────┘ └───────────────────────┘ +``` + +## Quick Start + +### MongoDB Connection + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' + +// Create connection +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game', + pool: { + minSize: 5, + maxSize: 20 + }, + autoReconnect: true +}) + +// Listen to events +mongo.on('connected', () => console.log('MongoDB connected')) +mongo.on('disconnected', () => console.log('MongoDB disconnected')) +mongo.on('error', (e) => console.error('Error:', e.error)) + +// Connect +await mongo.connect() + +// Use collections +const users = mongo.collection('users') +await users.insertOne({ name: 'John', score: 100 }) + +const user = await users.findOne({ name: 'John' }) + +// Disconnect when done +await mongo.disconnect() +``` + +### Redis Connection + +```typescript +import { createRedisConnection } from '@esengine/database-drivers' + +const redis = createRedisConnection({ + host: 'localhost', + port: 6379, + keyPrefix: 'game:', + autoReconnect: true +}) + +await redis.connect() + +// Basic operations +await redis.set('session:123', 'data', 3600) // With TTL +const value = await redis.get('session:123') + +await redis.disconnect() +``` + +## Service Container Integration + +```typescript +import { ServiceContainer } from '@esengine/ecs-framework' +import { + createMongoConnection, + MongoConnectionToken, + RedisConnectionToken +} from '@esengine/database-drivers' + +const services = new ServiceContainer() + +// Register connections +const mongo = createMongoConnection({ uri: '...', database: 'game' }) +await mongo.connect() +services.register(MongoConnectionToken, mongo) + +// Retrieve in other modules +const connection = services.get(MongoConnectionToken) +const users = connection.collection('users') +``` + +## Documentation + +- [MongoDB Connection](/en/modules/database-drivers/mongo/) - MongoDB configuration details +- [Redis Connection](/en/modules/database-drivers/redis/) - Redis configuration details +- [Service Tokens](/en/modules/database-drivers/tokens/) - Dependency injection integration diff --git a/docs/src/content/docs/en/modules/database/index.md b/docs/src/content/docs/en/modules/database/index.md new file mode 100644 index 00000000..082f439c --- /dev/null +++ b/docs/src/content/docs/en/modules/database/index.md @@ -0,0 +1,217 @@ +--- +title: "Database Repository" +description: "Repository pattern database layer with CRUD, pagination, and soft delete" +--- + +`@esengine/database` is ESEngine's database operation layer, providing type-safe CRUD operations based on the Repository pattern. + +## Features + +- **Repository Pattern** - Generic CRUD operations with type safety +- **Pagination** - Built-in pagination support +- **Soft Delete** - Optional soft delete with restore +- **User Management** - Ready-to-use UserRepository +- **Password Security** - Secure password hashing with scrypt + +## Installation + +```bash +npm install @esengine/database @esengine/database-drivers +``` + +## Quick Start + +### Basic Repository + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { Repository, createRepository } from '@esengine/database' + +// Define entity +interface Player { + id: string + name: string + score: number + createdAt: Date + updatedAt: Date +} + +// Create connection +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +// Create repository +const playerRepo = createRepository(mongo, 'players') + +// CRUD operations +const player = await playerRepo.create({ + name: 'John', + score: 0 +}) + +const found = await playerRepo.findById(player.id) + +await playerRepo.update(player.id, { score: 100 }) + +await playerRepo.delete(player.id) +``` + +### Custom Repository + +```typescript +import { Repository, BaseEntity } from '@esengine/database' +import type { IMongoConnection } from '@esengine/database-drivers' + +interface Player extends BaseEntity { + name: string + score: number + rank?: string +} + +class PlayerRepository extends Repository { + constructor(connection: IMongoConnection) { + super(connection, 'players') + } + + async findTopPlayers(limit: number = 10): Promise { + return this.findMany({ + sort: { score: 'desc' }, + limit + }) + } + + async findByRank(rank: string): Promise { + return this.findMany({ + where: { rank } + }) + } +} + +// Usage +const playerRepo = new PlayerRepository(mongo) +const topPlayers = await playerRepo.findTopPlayers(5) +``` + +### User Repository + +```typescript +import { UserRepository } from '@esengine/database' + +const userRepo = new UserRepository(mongo) + +// Register new user +const user = await userRepo.register({ + username: 'john', + password: 'securePassword123', + email: 'john@example.com' +}) + +// Authenticate +const authenticated = await userRepo.authenticate('john', 'securePassword123') +if (authenticated) { + console.log('Login successful:', authenticated.username) +} + +// Change password +await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456') + +// Role management +await userRepo.addRole(user.id, 'admin') +await userRepo.removeRole(user.id, 'admin') + +// Find users +const admins = await userRepo.findByRole('admin') +const john = await userRepo.findByUsername('john') +``` + +### Pagination + +```typescript +const result = await playerRepo.findPaginated( + { page: 1, pageSize: 20 }, + { + where: { rank: 'gold' }, + sort: { score: 'desc' } + } +) + +console.log(result.data) // Player[] +console.log(result.total) // Total count +console.log(result.totalPages) // Total pages +console.log(result.hasNext) // Has next page +console.log(result.hasPrev) // Has previous page +``` + +### Soft Delete + +```typescript +// Enable soft delete +const playerRepo = createRepository(mongo, 'players', true) + +// Delete (marks as deleted) +await playerRepo.delete(playerId) + +// Find excludes soft-deleted by default +const players = await playerRepo.findMany() + +// Include soft-deleted records +const allPlayers = await playerRepo.findMany({ + includeSoftDeleted: true +}) + +// Restore soft-deleted record +await playerRepo.restore(playerId) +``` + +### Query Options + +```typescript +// Complex queries +const players = await playerRepo.findMany({ + where: { + score: { $gte: 100 }, + rank: { $in: ['gold', 'platinum'] }, + name: { $like: 'John%' } + }, + sort: { + score: 'desc', + name: 'asc' + }, + limit: 10, + offset: 0 +}) + +// OR conditions +const players = await playerRepo.findMany({ + where: { + $or: [ + { score: { $gte: 1000 } }, + { rank: 'legendary' } + ] + } +}) +``` + +## Query Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `$eq` | Equal | `{ score: { $eq: 100 } }` | +| `$ne` | Not equal | `{ status: { $ne: 'banned' } }` | +| `$gt` | Greater than | `{ score: { $gt: 50 } }` | +| `$gte` | Greater or equal | `{ level: { $gte: 10 } }` | +| `$lt` | Less than | `{ age: { $lt: 18 } }` | +| `$lte` | Less or equal | `{ price: { $lte: 100 } }` | +| `$in` | In array | `{ rank: { $in: ['gold', 'platinum'] } }` | +| `$nin` | Not in array | `{ status: { $nin: ['banned'] } }` | +| `$like` | Pattern match | `{ name: { $like: '%john%' } }` | +| `$regex` | Regex match | `{ email: { $regex: '@gmail.com$' } }` | + +## Documentation + +- [Repository API](/en/modules/database/repository/) - Repository detailed API +- [User Management](/en/modules/database/user/) - UserRepository usage +- [Query Syntax](/en/modules/database/query/) - Query condition syntax diff --git a/docs/src/content/docs/en/modules/index.md b/docs/src/content/docs/en/modules/index.md index 2ac1c8b7..8067341e 100644 --- a/docs/src/content/docs/en/modules/index.md +++ b/docs/src/content/docs/en/modules/index.md @@ -36,6 +36,13 @@ ESEngine provides a rich set of modules that can be imported as needed. | [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking | | [Transaction](/en/modules/transaction/) | `@esengine/transaction` | Game transactions with distributed support | +### Database + +| Module | Package | Description | +|--------|---------|-------------| +| [Database Drivers](/en/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB, Redis connection management | +| [Database Repository](/en/modules/database/) | `@esengine/database` | Repository pattern data operations | + ## Installation All modules can be installed independently: diff --git a/docs/src/content/docs/en/modules/transaction/storage.md b/docs/src/content/docs/en/modules/transaction/storage.md index ffd093f3..b99051f6 100644 --- a/docs/src/content/docs/en/modules/transaction/storage.md +++ b/docs/src/content/docs/en/modules/transaction/storage.md @@ -125,23 +125,24 @@ tx:data:{key} - Business data ## MongoStorage -MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses factory pattern with lazy connection. +MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses shared connection from `@esengine/database-drivers`. ```typescript -import { MongoClient } from 'mongodb'; -import { MongoStorage } from '@esengine/transaction'; +import { createMongoConnection } from '@esengine/database-drivers'; +import { createMongoStorage, TransactionManager } from '@esengine/transaction'; -// Factory pattern: lazy connection, connects on first operation -const storage = new MongoStorage({ - factory: async () => { - const client = new MongoClient('mongodb://localhost:27017'); - await client.connect(); - return client; - }, - database: 'game', - transactionCollection: 'transactions', // Transaction log collection - dataCollection: 'transaction_data', // Business data collection - lockCollection: 'transaction_locks', // Lock collection +// Create shared connection +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}); +await mongo.connect(); + +// Create storage using shared connection +const storage = createMongoStorage(mongo, { + transactionCollection: 'transactions', // Transaction log collection (optional) + dataCollection: 'transaction_data', // Business data collection (optional) + lockCollection: 'transaction_locks', // Lock collection (optional) }); // Create indexes (run on first startup) @@ -149,11 +150,14 @@ await storage.ensureIndexes(); const manager = new TransactionManager({ storage }); -// Close connection when done +// Close storage (does not close shared connection) await storage.close(); -// Or use await using for automatic cleanup (TypeScript 5.2+) -await using storage = new MongoStorage({ ... }); +// Shared connection can continue to be used by other modules +const userRepo = new UserRepository(mongo); // @esengine/database + +// Finally close the shared connection +await mongo.disconnect(); ``` ### Characteristics diff --git a/docs/src/content/docs/modules/database-drivers/index.md b/docs/src/content/docs/modules/database-drivers/index.md new file mode 100644 index 00000000..ec40339a --- /dev/null +++ b/docs/src/content/docs/modules/database-drivers/index.md @@ -0,0 +1,136 @@ +--- +title: "数据库驱动" +description: "MongoDB、Redis 等数据库的连接管理和驱动封装" +--- + +`@esengine/database-drivers` 是 ESEngine 的数据库连接管理层,提供 MongoDB、Redis 等数据库的统一连接管理。 + +## 特性 + +- **连接池管理** - 自动管理连接池,优化资源使用 +- **自动重连** - 连接断开时自动重连 +- **事件通知** - 连接状态变化事件 +- **类型解耦** - 简化接口,不依赖原生驱动类型 +- **共享连接** - 单一连接可供多个模块共享 + +## 安装 + +```bash +npm install @esengine/database-drivers +``` + +**对等依赖:** +```bash +npm install mongodb # MongoDB 支持 +npm install ioredis # Redis 支持 +``` + +## 架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ @esengine/database-drivers (Layer 1) │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ MongoConnection │ │ RedisConnection │ │ +│ │ - 连接池管理 │ │ - 自动重连 │ │ +│ │ - 自动重连 │ │ - Key 前缀 │ │ +│ │ - 事件发射器 │ │ - 事件发射器 │ │ +│ └──────────┬──────────┘ └─────────────────────┘ │ +│ │ │ +│ ┌──────────▼──────────┐ │ +│ │ IMongoCollection │ ← 类型安全接口 │ +│ │ (适配器模式) │ 与 mongodb 类型解耦 │ +│ └─────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌───────────────────────┐ ┌───────────────────────┐ +│ @esengine/database │ │ @esengine/transaction │ +│ (仓库模式) │ │ (分布式事务) │ +└───────────────────────┘ └───────────────────────┘ +``` + +## 快速开始 + +### MongoDB 连接 + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' + +// 创建连接 +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game', + pool: { + minSize: 5, + maxSize: 20 + }, + autoReconnect: true +}) + +// 监听事件 +mongo.on('connected', () => console.log('MongoDB 已连接')) +mongo.on('disconnected', () => console.log('MongoDB 已断开')) +mongo.on('error', (e) => console.error('错误:', e.error)) + +// 建立连接 +await mongo.connect() + +// 使用集合 +const users = mongo.collection('users') +await users.insertOne({ name: 'John', score: 100 }) + +const user = await users.findOne({ name: 'John' }) + +// 完成后断开连接 +await mongo.disconnect() +``` + +### Redis 连接 + +```typescript +import { createRedisConnection } from '@esengine/database-drivers' + +const redis = createRedisConnection({ + host: 'localhost', + port: 6379, + keyPrefix: 'game:', + autoReconnect: true +}) + +await redis.connect() + +// 基本操作 +await redis.set('session:123', 'data', 3600) // 带 TTL +const value = await redis.get('session:123') + +await redis.disconnect() +``` + +## 服务容器集成 + +```typescript +import { ServiceContainer } from '@esengine/ecs-framework' +import { + createMongoConnection, + MongoConnectionToken, + RedisConnectionToken +} from '@esengine/database-drivers' + +const services = new ServiceContainer() + +// 注册连接 +const mongo = createMongoConnection({ uri: '...', database: 'game' }) +await mongo.connect() +services.register(MongoConnectionToken, mongo) + +// 在其他模块中获取 +const connection = services.get(MongoConnectionToken) +const users = connection.collection('users') +``` + +## 文档 + +- [MongoDB 连接](/modules/database-drivers/mongo/) - MongoDB 连接详细配置 +- [Redis 连接](/modules/database-drivers/redis/) - Redis 连接详细配置 +- [服务令牌](/modules/database-drivers/tokens/) - 依赖注入集成 diff --git a/docs/src/content/docs/modules/database-drivers/mongo.md b/docs/src/content/docs/modules/database-drivers/mongo.md new file mode 100644 index 00000000..dc24ec80 --- /dev/null +++ b/docs/src/content/docs/modules/database-drivers/mongo.md @@ -0,0 +1,265 @@ +--- +title: "MongoDB 连接" +description: "MongoDB 连接管理、连接池、自动重连" +--- + +## 配置选项 + +```typescript +interface MongoConnectionConfig { + /** MongoDB 连接 URI */ + uri: string + + /** 数据库名称 */ + database: string + + /** 连接池配置 */ + pool?: { + minSize?: number // 最小连接数 + maxSize?: number // 最大连接数 + acquireTimeout?: number // 获取连接超时(毫秒) + maxLifetime?: number // 连接最大生命周期(毫秒) + } + + /** 是否自动重连(默认 true) */ + autoReconnect?: boolean + + /** 重连间隔(毫秒,默认 5000) */ + reconnectInterval?: number + + /** 最大重连次数(默认 10) */ + maxReconnectAttempts?: number +} +``` + +## 完整示例 + +```typescript +import { createMongoConnection, MongoConnectionToken } from '@esengine/database-drivers' + +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game', + pool: { + minSize: 5, + maxSize: 20, + acquireTimeout: 5000, + maxLifetime: 300000 + }, + autoReconnect: true, + reconnectInterval: 5000, + maxReconnectAttempts: 10 +}) + +// 事件监听 +mongo.on('connected', () => { + console.log('MongoDB 已连接') +}) + +mongo.on('disconnected', () => { + console.log('MongoDB 已断开') +}) + +mongo.on('reconnecting', () => { + console.log('MongoDB 正在重连...') +}) + +mongo.on('reconnected', () => { + console.log('MongoDB 重连成功') +}) + +mongo.on('error', (event) => { + console.error('MongoDB 错误:', event.error) +}) + +// 连接 +await mongo.connect() + +// 检查状态 +console.log('已连接:', mongo.isConnected()) +console.log('Ping:', await mongo.ping()) +``` + +## IMongoConnection 接口 + +```typescript +interface IMongoConnection { + /** 连接 ID */ + readonly id: string + + /** 连接状态 */ + readonly state: ConnectionState + + /** 建立连接 */ + connect(): Promise + + /** 断开连接 */ + disconnect(): Promise + + /** 检查是否已连接 */ + isConnected(): boolean + + /** 测试连接 */ + ping(): Promise + + /** 获取类型化集合 */ + collection(name: string): IMongoCollection + + /** 获取数据库接口 */ + getDatabase(): IMongoDatabase + + /** 获取原生客户端(高级用法) */ + getNativeClient(): MongoClientType + + /** 获取原生数据库(高级用法) */ + getNativeDatabase(): Db +} +``` + +## IMongoCollection 接口 + +类型安全的集合接口,与原生 MongoDB 类型解耦: + +```typescript +interface IMongoCollection { + readonly name: string + + // 查询 + findOne(filter: object, options?: FindOptions): Promise + find(filter: object, options?: FindOptions): Promise + countDocuments(filter?: object): Promise + + // 插入 + insertOne(doc: T): Promise + insertMany(docs: T[]): Promise + + // 更新 + updateOne(filter: object, update: object): Promise + updateMany(filter: object, update: object): Promise + findOneAndUpdate( + filter: object, + update: object, + options?: FindOneAndUpdateOptions + ): Promise + + // 删除 + deleteOne(filter: object): Promise + deleteMany(filter: object): Promise + + // 索引 + createIndex( + spec: Record, + options?: IndexOptions + ): Promise +} +``` + +## 使用示例 + +### 基本 CRUD + +```typescript +interface User { + id: string + name: string + email: string + score: number +} + +const users = mongo.collection('users') + +// 插入 +await users.insertOne({ + id: '1', + name: 'John', + email: 'john@example.com', + score: 100 +}) + +// 查询 +const user = await users.findOne({ name: 'John' }) + +const topUsers = await users.find( + { score: { $gte: 100 } }, + { sort: { score: -1 }, limit: 10 } +) + +// 更新 +await users.updateOne( + { id: '1' }, + { $inc: { score: 10 } } +) + +// 删除 +await users.deleteOne({ id: '1' }) +``` + +### 批量操作 + +```typescript +// 批量插入 +await users.insertMany([ + { id: '1', name: 'Alice', email: 'alice@example.com', score: 100 }, + { id: '2', name: 'Bob', email: 'bob@example.com', score: 200 }, + { id: '3', name: 'Carol', email: 'carol@example.com', score: 150 } +]) + +// 批量更新 +await users.updateMany( + { score: { $lt: 100 } }, + { $set: { status: 'inactive' } } +) + +// 批量删除 +await users.deleteMany({ status: 'inactive' }) +``` + +### 索引管理 + +```typescript +// 创建索引 +await users.createIndex({ email: 1 }, { unique: true }) +await users.createIndex({ score: -1 }) +await users.createIndex({ name: 1, score: -1 }) +``` + +## 与其他模块集成 + +### 与 @esengine/database 集成 + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { UserRepository, createRepository } from '@esengine/database' + +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +// 使用 UserRepository +const userRepo = new UserRepository(mongo) +await userRepo.register({ username: 'john', password: '123456' }) + +// 使用通用仓库 +const playerRepo = createRepository(mongo, 'players') +``` + +### 与 @esengine/transaction 集成 + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { createMongoStorage, TransactionManager } from '@esengine/transaction' + +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +// 创建事务存储(共享连接) +const storage = createMongoStorage(mongo) +await storage.ensureIndexes() + +const txManager = new TransactionManager({ storage }) +``` diff --git a/docs/src/content/docs/modules/database-drivers/redis.md b/docs/src/content/docs/modules/database-drivers/redis.md new file mode 100644 index 00000000..355c51b2 --- /dev/null +++ b/docs/src/content/docs/modules/database-drivers/redis.md @@ -0,0 +1,228 @@ +--- +title: "Redis 连接" +description: "Redis 连接管理、自动重连、键前缀" +--- + +## 配置选项 + +```typescript +interface RedisConnectionConfig { + /** Redis 主机 */ + host?: string + + /** Redis 端口 */ + port?: number + + /** 认证密码 */ + password?: string + + /** 数据库编号 */ + db?: number + + /** 键前缀 */ + keyPrefix?: string + + /** 是否自动重连(默认 true) */ + autoReconnect?: boolean + + /** 重连间隔(毫秒,默认 5000) */ + reconnectInterval?: number + + /** 最大重连次数(默认 10) */ + maxReconnectAttempts?: number +} +``` + +## 完整示例 + +```typescript +import { createRedisConnection, RedisConnectionToken } from '@esengine/database-drivers' + +const redis = createRedisConnection({ + host: 'localhost', + port: 6379, + password: 'your-password', + db: 0, + keyPrefix: 'game:', + autoReconnect: true, + reconnectInterval: 5000, + maxReconnectAttempts: 10 +}) + +// 事件监听 +redis.on('connected', () => { + console.log('Redis 已连接') +}) + +redis.on('disconnected', () => { + console.log('Redis 已断开') +}) + +redis.on('error', (event) => { + console.error('Redis 错误:', event.error) +}) + +// 连接 +await redis.connect() + +// 检查状态 +console.log('已连接:', redis.isConnected()) +console.log('Ping:', await redis.ping()) +``` + +## IRedisConnection 接口 + +```typescript +interface IRedisConnection { + /** 连接 ID */ + readonly id: string + + /** 连接状态 */ + readonly state: ConnectionState + + /** 建立连接 */ + connect(): Promise + + /** 断开连接 */ + disconnect(): Promise + + /** 检查是否已连接 */ + isConnected(): boolean + + /** 测试连接 */ + ping(): Promise + + /** 获取值 */ + get(key: string): Promise + + /** 设置值(可选 TTL,单位秒) */ + set(key: string, value: string, ttl?: number): Promise + + /** 删除键 */ + del(key: string): Promise + + /** 检查键是否存在 */ + exists(key: string): Promise + + /** 设置过期时间(秒) */ + expire(key: string, seconds: number): Promise + + /** 获取剩余过期时间(秒) */ + ttl(key: string): Promise + + /** 获取原生客户端(高级用法) */ + getNativeClient(): Redis +} +``` + +## 使用示例 + +### 基本操作 + +```typescript +// 设置值 +await redis.set('user:1:name', 'John') + +// 设置带过期时间的值(1 小时) +await redis.set('session:abc123', 'user-data', 3600) + +// 获取值 +const name = await redis.get('user:1:name') + +// 检查键是否存在 +const exists = await redis.exists('user:1:name') + +// 删除键 +await redis.del('user:1:name') + +// 获取剩余过期时间 +const ttl = await redis.ttl('session:abc123') +``` + +### 键前缀 + +配置 `keyPrefix` 后,所有操作自动添加前缀: + +```typescript +const redis = createRedisConnection({ + host: 'localhost', + keyPrefix: 'game:' +}) + +// 实际操作的键是 'game:user:1' +await redis.set('user:1', 'data') + +// 实际查询的键是 'game:user:1' +const data = await redis.get('user:1') +``` + +### 高级操作 + +使用原生客户端进行高级操作: + +```typescript +const client = redis.getNativeClient() + +// 使用 Pipeline +const pipeline = client.pipeline() +pipeline.set('key1', 'value1') +pipeline.set('key2', 'value2') +pipeline.set('key3', 'value3') +await pipeline.exec() + +// 使用事务 +const multi = client.multi() +multi.incr('counter') +multi.get('counter') +const results = await multi.exec() + +// 使用 Lua 脚本 +const result = await client.eval( + `return redis.call('get', KEYS[1])`, + 1, + 'mykey' +) +``` + +## 与事务系统集成 + +```typescript +import { createRedisConnection } from '@esengine/database-drivers' +import { RedisStorage, TransactionManager } from '@esengine/transaction' + +const redis = createRedisConnection({ + host: 'localhost', + port: 6379, + keyPrefix: 'tx:' +}) +await redis.connect() + +// 创建事务存储 +const storage = new RedisStorage({ + factory: () => redis.getNativeClient(), + prefix: 'tx:' +}) + +const txManager = new TransactionManager({ storage }) +``` + +## 连接状态 + +```typescript +type ConnectionState = + | 'disconnected' // 未连接 + | 'connecting' // 连接中 + | 'connected' // 已连接 + | 'disconnecting' // 断开中 + | 'error' // 错误状态 +``` + +## 事件 + +| 事件 | 描述 | +|------|------| +| `connected` | 连接成功 | +| `disconnected` | 连接断开 | +| `reconnecting` | 正在重连 | +| `reconnected` | 重连成功 | +| `error` | 发生错误 | diff --git a/docs/src/content/docs/modules/database/index.md b/docs/src/content/docs/modules/database/index.md new file mode 100644 index 00000000..6c729769 --- /dev/null +++ b/docs/src/content/docs/modules/database/index.md @@ -0,0 +1,140 @@ +--- +title: "数据库仓库" +description: "Repository 模式的数据库操作层,支持 CRUD、分页、软删除" +--- + +`@esengine/database` 是 ESEngine 的数据库操作层,基于 Repository 模式提供类型安全的 CRUD 操作。 + +## 特性 + +- **Repository 模式** - 泛型 CRUD 操作,类型安全 +- **分页查询** - 内置分页支持 +- **软删除** - 可选的软删除与恢复 +- **用户管理** - 开箱即用的 UserRepository +- **密码安全** - 使用 scrypt 的密码哈希工具 + +## 安装 + +```bash +npm install @esengine/database @esengine/database-drivers +``` + +## 快速开始 + +### 基本仓库 + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { Repository, createRepository } from '@esengine/database' + +// 定义实体 +interface Player { + id: string + name: string + score: number + createdAt: Date + updatedAt: Date +} + +// 创建连接 +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +// 创建仓库 +const playerRepo = createRepository(mongo, 'players') + +// CRUD 操作 +const player = await playerRepo.create({ + name: 'John', + score: 0 +}) + +const found = await playerRepo.findById(player.id) + +await playerRepo.update(player.id, { score: 100 }) + +await playerRepo.delete(player.id) +``` + +### 自定义仓库 + +```typescript +import { Repository, BaseEntity } from '@esengine/database' +import type { IMongoConnection } from '@esengine/database-drivers' + +interface Player extends BaseEntity { + name: string + score: number + rank?: string +} + +class PlayerRepository extends Repository { + constructor(connection: IMongoConnection) { + super(connection, 'players') + } + + async findTopPlayers(limit: number = 10): Promise { + return this.findMany({ + sort: { score: 'desc' }, + limit + }) + } + + async findByRank(rank: string): Promise { + return this.findMany({ + where: { rank } + }) + } + + async incrementScore(playerId: string, amount: number): Promise { + const player = await this.findById(playerId) + if (!player) return null + return this.update(playerId, { score: player.score + amount }) + } +} + +// 使用 +const playerRepo = new PlayerRepository(mongo) +const topPlayers = await playerRepo.findTopPlayers(5) +``` + +### 用户仓库 + +```typescript +import { UserRepository } from '@esengine/database' + +const userRepo = new UserRepository(mongo) + +// 注册新用户 +const user = await userRepo.register({ + username: 'john', + password: 'securePassword123', + email: 'john@example.com' +}) + +// 认证 +const authenticated = await userRepo.authenticate('john', 'securePassword123') +if (authenticated) { + console.log('登录成功:', authenticated.username) +} + +// 修改密码 +await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456') + +// 角色管理 +await userRepo.addRole(user.id, 'admin') +await userRepo.removeRole(user.id, 'admin') + +// 查询用户 +const admins = await userRepo.findByRole('admin') +const john = await userRepo.findByUsername('john') +``` + +## 文档 + +- [仓库 API](/modules/database/repository/) - Repository 详细 API +- [用户管理](/modules/database/user/) - UserRepository 用法 +- [查询语法](/modules/database/query/) - 查询条件语法 diff --git a/docs/src/content/docs/modules/database/query.md b/docs/src/content/docs/modules/database/query.md new file mode 100644 index 00000000..470e79d7 --- /dev/null +++ b/docs/src/content/docs/modules/database/query.md @@ -0,0 +1,185 @@ +--- +title: "查询语法" +description: "查询条件操作符和语法" +--- + +## 基本查询 + +### 精确匹配 + +```typescript +await repo.findMany({ + where: { + name: 'John', + status: 'active' + } +}) +``` + +### 使用操作符 + +```typescript +await repo.findMany({ + where: { + score: { $gte: 100 }, + rank: { $in: ['gold', 'platinum'] } + } +}) +``` + +## 查询操作符 + +| 操作符 | 描述 | 示例 | +|--------|------|------| +| `$eq` | 等于 | `{ score: { $eq: 100 } }` | +| `$ne` | 不等于 | `{ status: { $ne: 'banned' } }` | +| `$gt` | 大于 | `{ score: { $gt: 50 } }` | +| `$gte` | 大于等于 | `{ level: { $gte: 10 } }` | +| `$lt` | 小于 | `{ age: { $lt: 18 } }` | +| `$lte` | 小于等于 | `{ price: { $lte: 100 } }` | +| `$in` | 在数组中 | `{ rank: { $in: ['gold', 'platinum'] } }` | +| `$nin` | 不在数组中 | `{ status: { $nin: ['banned', 'suspended'] } }` | +| `$like` | 模式匹配 | `{ name: { $like: '%john%' } }` | +| `$regex` | 正则匹配 | `{ email: { $regex: '@gmail.com$' } }` | + +## 逻辑操作符 + +### $or + +```typescript +await repo.findMany({ + where: { + $or: [ + { score: { $gte: 1000 } }, + { rank: 'legendary' } + ] + } +}) +``` + +### $and + +```typescript +await repo.findMany({ + where: { + $and: [ + { score: { $gte: 100 } }, + { score: { $lte: 500 } } + ] + } +}) +``` + +### 组合使用 + +```typescript +await repo.findMany({ + where: { + status: 'active', + $or: [ + { rank: 'gold' }, + { score: { $gte: 1000 } } + ] + } +}) +``` + +## 模式匹配 + +### $like 语法 + +- `%` - 匹配任意字符序列 +- `_` - 匹配单个字符 + +```typescript +// 以 'John' 开头 +{ name: { $like: 'John%' } } + +// 以 'son' 结尾 +{ name: { $like: '%son' } } + +// 包含 'oh' +{ name: { $like: '%oh%' } } + +// 第二个字符是 'o' +{ name: { $like: '_o%' } } +``` + +### $regex 语法 + +使用标准正则表达式: + +```typescript +// 以 'John' 开头(大小写不敏感) +{ name: { $regex: '^john' } } + +// Gmail 邮箱 +{ email: { $regex: '@gmail\\.com$' } } + +// 包含数字 +{ username: { $regex: '\\d+' } } +``` + +## 排序 + +```typescript +await repo.findMany({ + sort: { + score: 'desc', // 降序 + name: 'asc' // 升序 + } +}) +``` + +## 分页 + +### 使用 limit/offset + +```typescript +// 第一页 +await repo.findMany({ + limit: 20, + offset: 0 +}) + +// 第二页 +await repo.findMany({ + limit: 20, + offset: 20 +}) +``` + +### 使用 findPaginated + +```typescript +const result = await repo.findPaginated( + { page: 2, pageSize: 20 }, + { sort: { createdAt: 'desc' } } +) +``` + +## 完整示例 + +```typescript +// 查找活跃的金牌玩家,分数在 100-1000 之间 +// 按分数降序排列,取前 10 个 +const players = await repo.findMany({ + where: { + status: 'active', + rank: 'gold', + score: { $gte: 100, $lte: 1000 } + }, + sort: { score: 'desc' }, + limit: 10 +}) + +// 搜索用户名包含 'john' 或邮箱是 gmail 的用户 +const users = await repo.findMany({ + where: { + $or: [ + { username: { $like: '%john%' } }, + { email: { $regex: '@gmail\\.com$' } } + ] + } +}) +``` diff --git a/docs/src/content/docs/modules/database/repository.md b/docs/src/content/docs/modules/database/repository.md new file mode 100644 index 00000000..4e31d9e9 --- /dev/null +++ b/docs/src/content/docs/modules/database/repository.md @@ -0,0 +1,244 @@ +--- +title: "Repository API" +description: "泛型仓库接口,CRUD 操作、分页、软删除" +--- + +## 创建仓库 + +### 使用工厂函数 + +```typescript +import { createRepository } from '@esengine/database' + +const playerRepo = createRepository(mongo, 'players') + +// 启用软删除 +const playerRepo = createRepository(mongo, 'players', true) +``` + +### 继承 Repository + +```typescript +import { Repository, BaseEntity } from '@esengine/database' + +interface Player extends BaseEntity { + name: string + score: number +} + +class PlayerRepository extends Repository { + constructor(connection: IMongoConnection) { + super(connection, 'players', false) // 第三个参数:启用软删除 + } + + // 添加自定义方法 + async findTopPlayers(limit: number): Promise { + return this.findMany({ + sort: { score: 'desc' }, + limit + }) + } +} +``` + +## BaseEntity 接口 + +所有实体必须继承 `BaseEntity`: + +```typescript +interface BaseEntity { + id: string + createdAt: Date + updatedAt: Date + deletedAt?: Date // 软删除时使用 +} +``` + +## 查询方法 + +### findById + +```typescript +const player = await repo.findById('player-123') +``` + +### findOne + +```typescript +const player = await repo.findOne({ + where: { name: 'John' } +}) + +const topPlayer = await repo.findOne({ + sort: { score: 'desc' } +}) +``` + +### findMany + +```typescript +// 简单查询 +const players = await repo.findMany({ + where: { rank: 'gold' } +}) + +// 复杂查询 +const players = await repo.findMany({ + where: { + score: { $gte: 100 }, + rank: { $in: ['gold', 'platinum'] } + }, + sort: { score: 'desc', name: 'asc' }, + limit: 10, + offset: 0 +}) +``` + +### findPaginated + +```typescript +const result = await repo.findPaginated( + { page: 1, pageSize: 20 }, + { + where: { rank: 'gold' }, + sort: { score: 'desc' } + } +) + +console.log(result.data) // Player[] +console.log(result.total) // 总数量 +console.log(result.totalPages) // 总页数 +console.log(result.hasNext) // 是否有下一页 +console.log(result.hasPrev) // 是否有上一页 +``` + +### count + +```typescript +const count = await repo.count({ + where: { rank: 'gold' } +}) +``` + +### exists + +```typescript +const exists = await repo.exists({ + where: { email: 'john@example.com' } +}) +``` + +## 创建方法 + +### create + +```typescript +const player = await repo.create({ + name: 'John', + score: 0 +}) +// 自动生成 id, createdAt, updatedAt +``` + +### createMany + +```typescript +const players = await repo.createMany([ + { name: 'Alice', score: 100 }, + { name: 'Bob', score: 200 }, + { name: 'Carol', score: 150 } +]) +``` + +## 更新方法 + +### update + +```typescript +const updated = await repo.update('player-123', { + score: 200, + rank: 'gold' +}) +// 自动更新 updatedAt +``` + +## 删除方法 + +### delete + +```typescript +// 普通删除 +await repo.delete('player-123') + +// 软删除(如果启用) +// 实际是设置 deletedAt 字段 +``` + +### deleteMany + +```typescript +const count = await repo.deleteMany({ + where: { score: { $lt: 10 } } +}) +``` + +## 软删除 + +### 启用软删除 + +```typescript +const repo = createRepository(mongo, 'players', true) +``` + +### 查询行为 + +```typescript +// 默认排除软删除记录 +const players = await repo.findMany() + +// 包含软删除记录 +const allPlayers = await repo.findMany({ + includeSoftDeleted: true +}) +``` + +### 恢复记录 + +```typescript +await repo.restore('player-123') +``` + +## QueryOptions + +```typescript +interface QueryOptions { + /** 查询条件 */ + where?: WhereCondition + + /** 排序 */ + sort?: Partial> + + /** 限制数量 */ + limit?: number + + /** 偏移量 */ + offset?: number + + /** 包含软删除记录(仅在启用软删除时有效) */ + includeSoftDeleted?: boolean +} +``` + +## PaginatedResult + +```typescript +interface PaginatedResult { + data: T[] + total: number + page: number + pageSize: number + totalPages: number + hasNext: boolean + hasPrev: boolean +} +``` diff --git a/docs/src/content/docs/modules/database/user.md b/docs/src/content/docs/modules/database/user.md new file mode 100644 index 00000000..bce97a31 --- /dev/null +++ b/docs/src/content/docs/modules/database/user.md @@ -0,0 +1,277 @@ +--- +title: "用户管理" +description: "UserRepository 用户注册、认证、角色管理" +--- + +## 概述 + +`UserRepository` 提供开箱即用的用户管理功能: + +- 用户注册与认证 +- 密码哈希(使用 scrypt) +- 角色管理 +- 账户状态管理 + +## 快速开始 + +```typescript +import { createMongoConnection } from '@esengine/database-drivers' +import { UserRepository } from '@esengine/database' + +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}) +await mongo.connect() + +const userRepo = new UserRepository(mongo) +``` + +## 用户注册 + +```typescript +const user = await userRepo.register({ + username: 'john', + password: 'securePassword123', + email: 'john@example.com', // 可选 + displayName: 'John Doe', // 可选 + roles: ['player'] // 可选,默认 [] +}) + +console.log(user) +// { +// id: 'uuid-...', +// username: 'john', +// email: 'john@example.com', +// displayName: 'John Doe', +// roles: ['player'], +// status: 'active', +// createdAt: Date, +// updatedAt: Date +// } +``` + +**注意**:`register` 返回的 `SafeUser` 不包含密码哈希。 + +## 用户认证 + +```typescript +const user = await userRepo.authenticate('john', 'securePassword123') + +if (user) { + console.log('登录成功:', user.username) +} else { + console.log('用户名或密码错误') +} +``` + +## 密码管理 + +### 修改密码 + +```typescript +const success = await userRepo.changePassword( + userId, + 'oldPassword123', + 'newPassword456' +) + +if (success) { + console.log('密码修改成功') +} else { + console.log('原密码错误') +} +``` + +### 重置密码 + +```typescript +// 管理员直接重置密码 +const success = await userRepo.resetPassword(userId, 'newPassword123') +``` + +## 角色管理 + +### 添加角色 + +```typescript +await userRepo.addRole(userId, 'admin') +await userRepo.addRole(userId, 'moderator') +``` + +### 移除角色 + +```typescript +await userRepo.removeRole(userId, 'moderator') +``` + +### 查询角色 + +```typescript +// 查找所有管理员 +const admins = await userRepo.findByRole('admin') + +// 检查用户是否有某角色 +const user = await userRepo.findById(userId) +const isAdmin = user?.roles.includes('admin') +``` + +## 查询用户 + +### 按用户名查找 + +```typescript +const user = await userRepo.findByUsername('john') +``` + +### 按邮箱查找 + +```typescript +const user = await userRepo.findByEmail('john@example.com') +``` + +### 按角色查找 + +```typescript +const admins = await userRepo.findByRole('admin') +``` + +### 使用继承的方法 + +```typescript +// 分页查询 +const result = await userRepo.findPaginated( + { page: 1, pageSize: 20 }, + { + where: { status: 'active' }, + sort: { createdAt: 'desc' } + } +) + +// 复杂查询 +const users = await userRepo.findMany({ + where: { + status: 'active', + roles: { $in: ['admin', 'moderator'] } + } +}) +``` + +## 账户状态 + +```typescript +type UserStatus = 'active' | 'inactive' | 'banned' | 'suspended' +``` + +### 更新状态 + +```typescript +await userRepo.update(userId, { status: 'banned' }) +``` + +### 查询特定状态 + +```typescript +const activeUsers = await userRepo.findMany({ + where: { status: 'active' } +}) + +const bannedUsers = await userRepo.findMany({ + where: { status: 'banned' } +}) +``` + +## 类型定义 + +### UserEntity + +```typescript +interface UserEntity extends BaseEntity { + username: string + passwordHash: string + email?: string + displayName?: string + roles: string[] + status: UserStatus + lastLoginAt?: Date +} +``` + +### SafeUser + +```typescript +type SafeUser = Omit +``` + +### CreateUserParams + +```typescript +interface CreateUserParams { + username: string + password: string + email?: string + displayName?: string + roles?: string[] +} +``` + +## 密码工具 + +独立的密码工具函数: + +```typescript +import { hashPassword, verifyPassword } from '@esengine/database' + +// 哈希密码 +const hash = await hashPassword('myPassword123') + +// 验证密码 +const isValid = await verifyPassword('myPassword123', hash) +``` + +### 安全说明 + +- 使用 Node.js 内置的 `scrypt` 算法 +- 自动生成随机盐值 +- 默认使用安全的迭代参数 +- 哈希格式:`salt:hash`(均为 hex 编码) + +## 扩展 UserRepository + +```typescript +import { UserRepository, UserEntity } from '@esengine/database' + +interface GameUser extends UserEntity { + level: number + experience: number + coins: number +} + +class GameUserRepository extends UserRepository { + // 重写集合名 + constructor(connection: IMongoConnection) { + super(connection, 'game_users') + } + + // 添加游戏相关方法 + async addExperience(userId: string, amount: number): Promise { + const user = await this.findById(userId) as GameUser | null + if (!user) return null + + const newExp = user.experience + amount + const newLevel = Math.floor(newExp / 1000) + 1 + + return this.update(userId, { + experience: newExp, + level: newLevel + }) as Promise + } + + async findTopPlayers(limit: number = 10): Promise { + return this.findMany({ + sort: { level: 'desc', experience: 'desc' }, + limit + }) as Promise + } +} +``` diff --git a/docs/src/content/docs/modules/index.md b/docs/src/content/docs/modules/index.md index f4e0aedd..9484c34f 100644 --- a/docs/src/content/docs/modules/index.md +++ b/docs/src/content/docs/modules/index.md @@ -37,6 +37,13 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中 | [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 | | [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 | +### 数据库模块 + +| 模块 | 包名 | 描述 | +|------|------|------| +| [数据库驱动](/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB、Redis 连接管理 | +| [数据库仓库](/modules/database/) | `@esengine/database` | Repository 模式数据操作 | + ## 安装 所有模块都可以独立安装: diff --git a/docs/src/content/docs/modules/transaction/storage.md b/docs/src/content/docs/modules/transaction/storage.md index aef700f2..8a724ed0 100644 --- a/docs/src/content/docs/modules/transaction/storage.md +++ b/docs/src/content/docs/modules/transaction/storage.md @@ -125,23 +125,24 @@ tx:data:{key} - 业务数据 ## MongoStorage -MongoDB 存储,适用于需要持久化和复杂查询的场景。使用工厂模式实现惰性连接。 +MongoDB 存储,适用于需要持久化和复杂查询的场景。使用 `@esengine/database-drivers` 的共享连接。 ```typescript -import { MongoClient } from 'mongodb'; -import { MongoStorage } from '@esengine/transaction'; +import { createMongoConnection } from '@esengine/database-drivers'; +import { createMongoStorage, TransactionManager } from '@esengine/transaction'; -// 工厂模式:惰性连接,首次操作时才创建连接 -const storage = new MongoStorage({ - factory: async () => { - const client = new MongoClient('mongodb://localhost:27017'); - await client.connect(); - return client; - }, - database: 'game', - transactionCollection: 'transactions', // 事务日志集合 - dataCollection: 'transaction_data', // 业务数据集合 - lockCollection: 'transaction_locks', // 锁集合 +// 创建共享连接 +const mongo = createMongoConnection({ + uri: 'mongodb://localhost:27017', + database: 'game' +}); +await mongo.connect(); + +// 使用共享连接创建存储 +const storage = createMongoStorage(mongo, { + transactionCollection: 'transactions', // 事务日志集合(可选) + dataCollection: 'transaction_data', // 业务数据集合(可选) + lockCollection: 'transaction_locks', // 锁集合(可选) }); // 创建索引(首次运行时执行) @@ -149,11 +150,14 @@ await storage.ensureIndexes(); const manager = new TransactionManager({ storage }); -// 使用后关闭连接 +// 关闭存储(不会关闭共享连接) await storage.close(); -// 或使用 await using 自动关闭 (TypeScript 5.2+) -await using storage = new MongoStorage({ ... }); +// 共享连接可继续用于其他模块 +const userRepo = new UserRepository(mongo); // @esengine/database + +// 最后关闭共享连接 +await mongo.disconnect(); ``` ### 特点 diff --git a/packages/framework/database-drivers/module.json b/packages/framework/database-drivers/module.json new file mode 100644 index 00000000..6cb99698 --- /dev/null +++ b/packages/framework/database-drivers/module.json @@ -0,0 +1,23 @@ +{ + "id": "database-drivers", + "name": "@esengine/database-drivers", + "globalKey": "database-drivers", + "displayName": "Database Drivers", + "description": "数据库连接驱动,提供 MongoDB、Redis 等数据库的连接管理 | Database connection drivers with connection pooling for MongoDB, Redis, etc.", + "version": "1.0.0", + "category": "Infrastructure", + "icon": "Database", + "tags": ["database", "mongodb", "redis", "connection"], + "isCore": false, + "defaultEnabled": true, + "isEngineModule": false, + "canContainContent": false, + "platforms": ["server"], + "dependencies": [], + "exports": { + "components": [], + "systems": [] + }, + "requiresWasm": false, + "outputPath": "dist/index.js" +} diff --git a/packages/framework/database-drivers/package.json b/packages/framework/database-drivers/package.json new file mode 100644 index 00000000..b8496a01 --- /dev/null +++ b/packages/framework/database-drivers/package.json @@ -0,0 +1,48 @@ +{ + "name": "@esengine/database-drivers", + "version": "1.0.0", + "description": "Database connection drivers for ESEngine | ESEngine 数据库连接驱动", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "module.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "peerDependencies": { + "mongodb": ">=6.0.0", + "ioredis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + }, + "ioredis": { + "optional": true + } + }, + "devDependencies": { + "@types/node": "^20.0.0", + "mongodb": "^6.12.0", + "ioredis": "^5.3.0", + "tsup": "^8.0.0", + "typescript": "^5.8.0", + "rimraf": "^5.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/framework/database-drivers/src/adapters/MongoCollectionAdapter.ts b/packages/framework/database-drivers/src/adapters/MongoCollectionAdapter.ts new file mode 100644 index 00000000..df9aac11 --- /dev/null +++ b/packages/framework/database-drivers/src/adapters/MongoCollectionAdapter.ts @@ -0,0 +1,238 @@ +/** + * @zh MongoDB 集合适配器 + * @en MongoDB collection adapter + * + * @zh 将 MongoDB 原生 Collection 适配为简化接口 + * @en Adapts native MongoDB Collection to simplified interface + */ + +import type { Collection, Db } from 'mongodb' +import type { + DeleteResult, + FindOneAndUpdateOptions, + FindOptions, + IMongoCollection, + IMongoDatabase, + IndexOptions, + InsertManyResult, + InsertOneResult, + UpdateResult +} from '../interfaces/IMongoCollection.js' + +/** + * @zh MongoDB 集合适配器 + * @en MongoDB collection adapter + */ +export class MongoCollectionAdapter implements IMongoCollection { + readonly name: string + + constructor(private readonly _collection: Collection) { + this.name = _collection.collectionName + } + + // ========================================================================= + // 查询 | Query + // ========================================================================= + + async findOne(filter: object, options?: FindOptions): Promise { + const doc = await this._collection.findOne( + filter as Parameters[0], + { + sort: options?.sort as Parameters[1] extends { sort?: infer S } ? S : never, + projection: options?.projection + } + ) + return doc ? this._stripId(doc) : null + } + + async find(filter: object, options?: FindOptions): Promise { + let cursor = this._collection.find( + filter as Parameters[0] + ) + + if (options?.sort) { + cursor = cursor.sort(options.sort as Parameters[0]) + } + + if (options?.skip) { + cursor = cursor.skip(options.skip) + } + + if (options?.limit) { + cursor = cursor.limit(options.limit) + } + + if (options?.projection) { + cursor = cursor.project(options.projection) + } + + const docs = await cursor.toArray() + return docs.map(doc => this._stripId(doc)) + } + + async countDocuments(filter?: object): Promise { + return this._collection.countDocuments( + (filter ?? {}) as Parameters[0] + ) + } + + // ========================================================================= + // 创建 | Create + // ========================================================================= + + async insertOne(doc: T): Promise { + const result = await this._collection.insertOne( + doc as Parameters[0] + ) + return { + insertedId: result.insertedId, + acknowledged: result.acknowledged + } + } + + async insertMany(docs: T[]): Promise { + const result = await this._collection.insertMany( + docs as Parameters[0] + ) + return { + insertedCount: result.insertedCount, + insertedIds: result.insertedIds as Record, + acknowledged: result.acknowledged + } + } + + // ========================================================================= + // 更新 | Update + // ========================================================================= + + async updateOne(filter: object, update: object): Promise { + const result = await this._collection.updateOne( + filter as Parameters[0], + update as Parameters[1] + ) + return { + matchedCount: result.matchedCount, + modifiedCount: result.modifiedCount, + upsertedCount: result.upsertedCount, + upsertedId: result.upsertedId, + acknowledged: result.acknowledged + } + } + + async updateMany(filter: object, update: object): Promise { + const result = await this._collection.updateMany( + filter as Parameters[0], + update as Parameters[1] + ) + return { + matchedCount: result.matchedCount, + modifiedCount: result.modifiedCount, + upsertedCount: result.upsertedCount, + upsertedId: result.upsertedId, + acknowledged: result.acknowledged + } + } + + async findOneAndUpdate( + filter: object, + update: object, + options?: FindOneAndUpdateOptions + ): Promise { + const result = await this._collection.findOneAndUpdate( + filter as Parameters[0], + update as Parameters[1], + { + returnDocument: options?.returnDocument ?? 'after', + upsert: options?.upsert + } + ) + return result ? this._stripId(result) : null + } + + // ========================================================================= + // 删除 | Delete + // ========================================================================= + + async deleteOne(filter: object): Promise { + const result = await this._collection.deleteOne( + filter as Parameters[0] + ) + return { + deletedCount: result.deletedCount, + acknowledged: result.acknowledged + } + } + + async deleteMany(filter: object): Promise { + const result = await this._collection.deleteMany( + filter as Parameters[0] + ) + return { + deletedCount: result.deletedCount, + acknowledged: result.acknowledged + } + } + + // ========================================================================= + // 索引 | Index + // ========================================================================= + + async createIndex( + spec: Record, + options?: IndexOptions + ): Promise { + return this._collection.createIndex(spec, options) + } + + // ========================================================================= + // 内部方法 | Internal Methods + // ========================================================================= + + /** + * @zh 移除 MongoDB 的 _id 字段 + * @en Remove MongoDB's _id field + */ + private _stripId(doc: D): D { + const { _id, ...rest } = doc as { _id?: unknown } & Record + return rest as D + } +} + +/** + * @zh MongoDB 数据库适配器 + * @en MongoDB database adapter + */ +export class MongoDatabaseAdapter implements IMongoDatabase { + readonly name: string + private _collections = new Map>() + + constructor(private readonly _db: Db) { + this.name = _db.databaseName + } + + collection(name: string): IMongoCollection { + if (!this._collections.has(name)) { + const nativeCollection = this._db.collection(name) + this._collections.set( + name, + new MongoCollectionAdapter(nativeCollection) as MongoCollectionAdapter + ) + } + return this._collections.get(name) as IMongoCollection + } + + async listCollections(): Promise { + const collections = await this._db.listCollections().toArray() + return collections.map(c => c.name) + } + + async dropCollection(name: string): Promise { + try { + await this._db.dropCollection(name) + this._collections.delete(name) + return true + } catch { + return false + } + } +} diff --git a/packages/framework/database-drivers/src/drivers/MongoConnection.ts b/packages/framework/database-drivers/src/drivers/MongoConnection.ts new file mode 100644 index 00000000..3d59d11f --- /dev/null +++ b/packages/framework/database-drivers/src/drivers/MongoConnection.ts @@ -0,0 +1,343 @@ +/** + * @zh MongoDB 连接驱动 + * @en MongoDB connection driver + * + * @zh 提供 MongoDB 数据库的连接管理、自动重连和事件通知 + * @en Provides MongoDB connection management, auto-reconnect, and event notification + */ + +import type { Db, MongoClient as MongoClientType, MongoClientOptions } from 'mongodb' +import { randomUUID } from 'crypto' +import { + ConnectionError, + type ConnectionEvent, + type ConnectionEventListener, + type ConnectionEventType, + type ConnectionState, + type IEventableConnection, + type MongoConnectionConfig +} from '../types.js' +import type { IMongoCollection, IMongoDatabase } from '../interfaces/IMongoCollection.js' +import { MongoDatabaseAdapter } from '../adapters/MongoCollectionAdapter.js' + +/** + * @zh MongoDB 连接接口 + * @en MongoDB connection interface + */ +export interface IMongoConnection extends IEventableConnection { + /** + * @zh 获取数据库接口 + * @en Get database interface + */ + getDatabase(): IMongoDatabase + + /** + * @zh 获取原生客户端(高级用法) + * @en Get native client (advanced usage) + */ + getNativeClient(): MongoClientType + + /** + * @zh 获取原生数据库(高级用法) + * @en Get native database (advanced usage) + */ + getNativeDatabase(): Db + + /** + * @zh 获取集合 + * @en Get collection + */ + collection(name: string): IMongoCollection +} + +/** + * @zh MongoDB 连接实现 + * @en MongoDB connection implementation + * + * @example + * ```typescript + * const mongo = new MongoConnection({ + * uri: 'mongodb://localhost:27017', + * database: 'game', + * autoReconnect: true, + * }) + * + * mongo.on('connected', () => console.log('Connected!')) + * mongo.on('error', (e) => console.error('Error:', e.error)) + * + * await mongo.connect() + * + * const users = mongo.collection('users') + * await users.insertOne({ name: 'test' }) + * + * await mongo.disconnect() + * ``` + */ +export class MongoConnection implements IMongoConnection { + readonly id: string + private _state: ConnectionState = 'disconnected' + private _client: MongoClientType | null = null + private _db: Db | null = null + private _config: MongoConnectionConfig + private _listeners = new Map>() + private _reconnectAttempts = 0 + private _reconnectTimer: ReturnType | null = null + + constructor(config: MongoConnectionConfig) { + this.id = randomUUID() + this._config = { + autoReconnect: true, + reconnectInterval: 5000, + maxReconnectAttempts: 10, + ...config + } + } + + // ========================================================================= + // 状态 | State + // ========================================================================= + + get state(): ConnectionState { + return this._state + } + + isConnected(): boolean { + return this._state === 'connected' && this._client !== null + } + + // ========================================================================= + // 连接管理 | Connection Management + // ========================================================================= + + async connect(): Promise { + if (this._state === 'connected') { + return + } + + if (this._state === 'connecting') { + throw new ConnectionError('Connection already in progress') + } + + this._state = 'connecting' + + try { + const { MongoClient } = await import('mongodb') + + const options: MongoClientOptions = {} + if (this._config.pool) { + if (this._config.pool.minSize) { + options.minPoolSize = this._config.pool.minSize + } + if (this._config.pool.maxSize) { + options.maxPoolSize = this._config.pool.maxSize + } + if (this._config.pool.acquireTimeout) { + options.waitQueueTimeoutMS = this._config.pool.acquireTimeout + } + if (this._config.pool.maxLifetime) { + options.maxIdleTimeMS = this._config.pool.maxLifetime + } + } + + this._client = new MongoClient(this._config.uri, options) + await this._client.connect() + this._db = this._client.db(this._config.database) + + this._state = 'connected' + this._reconnectAttempts = 0 + this._emit('connected') + + this._setupClientEvents() + } catch (error) { + this._state = 'error' + const connError = new ConnectionError( + `Failed to connect to MongoDB: ${(error as Error).message}`, + 'CONNECTION_FAILED', + error as Error + ) + this._emit('error', connError) + throw connError + } + } + + async disconnect(): Promise { + if (this._state === 'disconnected') { + return + } + + this._clearReconnectTimer() + this._state = 'disconnecting' + + try { + if (this._client) { + await this._client.close() + this._client = null + this._db = null + } + + this._state = 'disconnected' + this._emit('disconnected') + } catch (error) { + this._state = 'error' + throw new ConnectionError( + `Failed to disconnect: ${(error as Error).message}`, + 'CONNECTION_FAILED', + error as Error + ) + } + } + + async ping(): Promise { + if (!this._db) { + return false + } + + try { + await this._db.command({ ping: 1 }) + return true + } catch { + return false + } + } + + // ========================================================================= + // 数据库访问 | Database Access + // ========================================================================= + + private _dbAdapter: MongoDatabaseAdapter | null = null + + getDatabase(): IMongoDatabase { + if (!this._db) { + throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED') + } + if (!this._dbAdapter) { + this._dbAdapter = new MongoDatabaseAdapter(this._db) + } + return this._dbAdapter + } + + getNativeDatabase(): Db { + if (!this._db) { + throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED') + } + return this._db + } + + getNativeClient(): MongoClientType { + if (!this._client) { + throw new ConnectionError('Not connected to database', 'CONNECTION_CLOSED') + } + return this._client + } + + collection(name: string): IMongoCollection { + return this.getDatabase().collection(name) + } + + // ========================================================================= + // 事件 | Events + // ========================================================================= + + on(event: ConnectionEventType, listener: ConnectionEventListener): void { + if (!this._listeners.has(event)) { + this._listeners.set(event, new Set()) + } + this._listeners.get(event)!.add(listener) + } + + off(event: ConnectionEventType, listener: ConnectionEventListener): void { + this._listeners.get(event)?.delete(listener) + } + + once(event: ConnectionEventType, listener: ConnectionEventListener): void { + const wrapper: ConnectionEventListener = (e) => { + this.off(event, wrapper) + listener(e) + } + this.on(event, wrapper) + } + + private _emit(type: ConnectionEventType, error?: Error): void { + const event: ConnectionEvent = { + type, + connectionId: this.id, + timestamp: Date.now(), + error + } + + const listeners = this._listeners.get(type) + if (listeners) { + for (const listener of listeners) { + try { + listener(event) + } catch { + // Ignore listener errors + } + } + } + } + + // ========================================================================= + // 内部方法 | Internal Methods + // ========================================================================= + + private _setupClientEvents(): void { + if (!this._client) return + + this._client.on('close', () => { + if (this._state === 'connected') { + this._state = 'disconnected' + this._emit('disconnected') + this._scheduleReconnect() + } + }) + + this._client.on('error', (error) => { + this._emit('error', error) + }) + } + + private _scheduleReconnect(): void { + if (!this._config.autoReconnect) return + if (this._reconnectAttempts >= (this._config.maxReconnectAttempts ?? 10)) { + return + } + + this._clearReconnectTimer() + this._emit('reconnecting') + + this._reconnectTimer = setTimeout(async () => { + this._reconnectAttempts++ + try { + await this.connect() + this._emit('reconnected') + } catch { + this._scheduleReconnect() + } + }, this._config.reconnectInterval ?? 5000) + } + + private _clearReconnectTimer(): void { + if (this._reconnectTimer) { + clearTimeout(this._reconnectTimer) + this._reconnectTimer = null + } + } +} + +/** + * @zh 创建 MongoDB 连接 + * @en Create MongoDB connection + * + * @example + * ```typescript + * const mongo = createMongoConnection({ + * uri: process.env.MONGODB_URI!, + * database: 'game', + * }) + * await mongo.connect() + * ``` + */ +export function createMongoConnection(config: MongoConnectionConfig): MongoConnection { + return new MongoConnection(config) +} diff --git a/packages/framework/database-drivers/src/drivers/RedisConnection.ts b/packages/framework/database-drivers/src/drivers/RedisConnection.ts new file mode 100644 index 00000000..865379f1 --- /dev/null +++ b/packages/framework/database-drivers/src/drivers/RedisConnection.ts @@ -0,0 +1,300 @@ +/** + * @zh Redis 连接驱动 + * @en Redis connection driver + * + * @zh 提供 Redis 数据库的连接管理、自动重连和事件通知 + * @en Provides Redis connection management, auto-reconnect, and event notification + */ + +import type { Redis as RedisClientType, RedisOptions } from 'ioredis' +import { randomUUID } from 'crypto' +import { + ConnectionError, + type ConnectionEvent, + type ConnectionEventListener, + type ConnectionEventType, + type ConnectionState, + type IEventableConnection, + type RedisConnectionConfig +} from '../types.js' + +/** + * @zh Redis 连接接口 + * @en Redis connection interface + */ +export interface IRedisConnection extends IEventableConnection { + /** + * @zh 获取原生客户端 + * @en Get native client + */ + getClient(): RedisClientType + + /** + * @zh 获取键值 + * @en Get value by key + */ + get(key: string): Promise + + /** + * @zh 设置键值 + * @en Set key value + */ + set(key: string, value: string, ttl?: number): Promise + + /** + * @zh 删除键 + * @en Delete key + */ + del(key: string): Promise + + /** + * @zh 检查键是否存在 + * @en Check if key exists + */ + exists(key: string): Promise +} + +/** + * @zh Redis 连接实现 + * @en Redis connection implementation + * + * @example + * ```typescript + * const redis = new RedisConnection({ + * host: 'localhost', + * port: 6379, + * keyPrefix: 'game:', + * }) + * + * await redis.connect() + * + * await redis.set('player:1:score', '100', 3600) + * const score = await redis.get('player:1:score') + * + * await redis.disconnect() + * ``` + */ +export class RedisConnection implements IRedisConnection { + readonly id: string + private _state: ConnectionState = 'disconnected' + private _client: RedisClientType | null = null + private _config: RedisConnectionConfig + private _listeners = new Map>() + + constructor(config: RedisConnectionConfig) { + this.id = randomUUID() + this._config = { + host: 'localhost', + port: 6379, + autoReconnect: true, + ...config + } + } + + // ========================================================================= + // 状态 | State + // ========================================================================= + + get state(): ConnectionState { + return this._state + } + + isConnected(): boolean { + return this._state === 'connected' && this._client !== null + } + + // ========================================================================= + // 连接管理 | Connection Management + // ========================================================================= + + async connect(): Promise { + if (this._state === 'connected') { + return + } + + if (this._state === 'connecting') { + throw new ConnectionError('Connection already in progress') + } + + this._state = 'connecting' + + try { + const Redis = (await import('ioredis')).default + + const options: RedisOptions = { + host: this._config.host, + port: this._config.port, + password: this._config.password, + db: this._config.db, + keyPrefix: this._config.keyPrefix, + retryStrategy: this._config.autoReconnect + ? (times) => Math.min(times * 100, 3000) + : () => null, + lazyConnect: true + } + + if (this._config.url) { + this._client = new Redis(this._config.url, options) + } else { + this._client = new Redis(options) + } + + this._setupClientEvents() + await this._client.connect() + + this._state = 'connected' + this._emit('connected') + } catch (error) { + this._state = 'error' + const connError = new ConnectionError( + `Failed to connect to Redis: ${(error as Error).message}`, + 'CONNECTION_FAILED', + error as Error + ) + this._emit('error', connError) + throw connError + } + } + + async disconnect(): Promise { + if (this._state === 'disconnected') { + return + } + + this._state = 'disconnecting' + + try { + if (this._client) { + await this._client.quit() + this._client = null + } + + this._state = 'disconnected' + this._emit('disconnected') + } catch (error) { + this._state = 'error' + throw new ConnectionError( + `Failed to disconnect: ${(error as Error).message}`, + 'CONNECTION_FAILED', + error as Error + ) + } + } + + async ping(): Promise { + if (!this._client) { + return false + } + + try { + const result = await this._client.ping() + return result === 'PONG' + } catch { + return false + } + } + + // ========================================================================= + // 数据操作 | Data Operations + // ========================================================================= + + getClient(): RedisClientType { + if (!this._client) { + throw new ConnectionError('Not connected to Redis', 'CONNECTION_CLOSED') + } + return this._client + } + + async get(key: string): Promise { + return this.getClient().get(key) + } + + async set(key: string, value: string, ttl?: number): Promise { + const client = this.getClient() + if (ttl) { + await client.setex(key, ttl, value) + } else { + await client.set(key, value) + } + } + + async del(key: string): Promise { + const result = await this.getClient().del(key) + return result > 0 + } + + async exists(key: string): Promise { + const result = await this.getClient().exists(key) + return result > 0 + } + + // ========================================================================= + // 事件 | Events + // ========================================================================= + + on(event: ConnectionEventType, listener: ConnectionEventListener): void { + if (!this._listeners.has(event)) { + this._listeners.set(event, new Set()) + } + this._listeners.get(event)!.add(listener) + } + + off(event: ConnectionEventType, listener: ConnectionEventListener): void { + this._listeners.get(event)?.delete(listener) + } + + once(event: ConnectionEventType, listener: ConnectionEventListener): void { + const wrapper: ConnectionEventListener = (e) => { + this.off(event, wrapper) + listener(e) + } + this.on(event, wrapper) + } + + private _emit(type: ConnectionEventType, error?: Error): void { + const event: ConnectionEvent = { + type, + connectionId: this.id, + timestamp: Date.now(), + error + } + + const listeners = this._listeners.get(type) + if (listeners) { + for (const listener of listeners) { + try { + listener(event) + } catch { + // Ignore listener errors + } + } + } + } + + private _setupClientEvents(): void { + if (!this._client) return + + this._client.on('close', () => { + if (this._state === 'connected') { + this._state = 'disconnected' + this._emit('disconnected') + } + }) + + this._client.on('error', (error) => { + this._emit('error', error) + }) + + this._client.on('reconnecting', () => { + this._emit('reconnecting') + }) + } +} + +/** + * @zh 创建 Redis 连接 + * @en Create Redis connection + */ +export function createRedisConnection(config: RedisConnectionConfig): RedisConnection { + return new RedisConnection(config) +} diff --git a/packages/framework/database-drivers/src/drivers/index.ts b/packages/framework/database-drivers/src/drivers/index.ts new file mode 100644 index 00000000..7d4736be --- /dev/null +++ b/packages/framework/database-drivers/src/drivers/index.ts @@ -0,0 +1,29 @@ +/** + * @zh 数据库驱动导出 + * @en Database drivers export + */ + +export { + MongoConnection, + createMongoConnection, + type IMongoConnection +} from './MongoConnection.js' + +export { + RedisConnection, + createRedisConnection, + type IRedisConnection +} from './RedisConnection.js' + +// Re-export interfaces +export type { + IMongoCollection, + IMongoDatabase, + InsertOneResult, + InsertManyResult, + UpdateResult, + DeleteResult, + FindOptions, + FindOneAndUpdateOptions, + IndexOptions +} from '../interfaces/IMongoCollection.js' diff --git a/packages/framework/database-drivers/src/index.ts b/packages/framework/database-drivers/src/index.ts new file mode 100644 index 00000000..9cc2db5c --- /dev/null +++ b/packages/framework/database-drivers/src/index.ts @@ -0,0 +1,117 @@ +/** + * @zh @esengine/database-drivers 数据库连接驱动 + * @en @esengine/database-drivers Database Connection Drivers + * + * @zh 提供 MongoDB、Redis 等数据库的连接管理,支持连接池、自动重连和事件通知 + * @en Provides connection management for MongoDB, Redis, etc. with pooling, auto-reconnect, and events + * + * @example + * ```typescript + * import { + * createMongoConnection, + * createRedisConnection, + * MongoConnectionToken, + * RedisConnectionToken, + * } from '@esengine/database-drivers' + * + * // 创建 MongoDB 连接 + * const mongo = createMongoConnection({ + * uri: 'mongodb://localhost:27017', + * database: 'game', + * pool: { minSize: 5, maxSize: 20 }, + * autoReconnect: true, + * }) + * + * mongo.on('connected', () => console.log('MongoDB connected')) + * mongo.on('error', (e) => console.error('Error:', e.error)) + * + * await mongo.connect() + * + * // 直接使用 + * const users = mongo.collection('users') + * await users.insertOne({ name: 'test' }) + * + * // 或注册到服务容器供其他模块使用 + * services.register(MongoConnectionToken, mongo) + * + * // 创建 Redis 连接 + * const redis = createRedisConnection({ + * host: 'localhost', + * port: 6379, + * keyPrefix: 'game:', + * }) + * + * await redis.connect() + * await redis.set('session:123', 'data', 3600) + * + * // 断开连接 + * await mongo.disconnect() + * await redis.disconnect() + * ``` + */ + +// ============================================================================= +// Types | 类型 +// ============================================================================= + +export type { + ConnectionState, + IConnection, + IEventableConnection, + ConnectionEventType, + ConnectionEventListener, + ConnectionEvent, + PoolConfig, + MongoConnectionConfig, + RedisConnectionConfig, + DatabaseErrorCode +} from './types.js' + +export { + DatabaseError, + ConnectionError, + DuplicateKeyError +} from './types.js' + +// ============================================================================= +// Drivers | 驱动 +// ============================================================================= + +export { + MongoConnection, + createMongoConnection, + type IMongoConnection +} from './drivers/index.js' + +export { + RedisConnection, + createRedisConnection, + type IRedisConnection +} from './drivers/index.js' + +// ============================================================================= +// Interfaces | 接口 +// ============================================================================= + +export type { + IMongoCollection, + IMongoDatabase, + InsertOneResult, + InsertManyResult, + UpdateResult, + DeleteResult, + FindOptions, + FindOneAndUpdateOptions, + IndexOptions +} from './drivers/index.js' + +// ============================================================================= +// Tokens | 服务令牌 +// ============================================================================= + +export { + MongoConnectionToken, + RedisConnectionToken, + createServiceToken, + type ServiceToken +} from './tokens.js' diff --git a/packages/framework/database-drivers/src/interfaces/IMongoCollection.ts b/packages/framework/database-drivers/src/interfaces/IMongoCollection.ts new file mode 100644 index 00000000..1b9ddd06 --- /dev/null +++ b/packages/framework/database-drivers/src/interfaces/IMongoCollection.ts @@ -0,0 +1,237 @@ +/** + * @zh MongoDB 集合简化接口 + * @en MongoDB collection simplified interface + * + * @zh 提供与 MongoDB 解耦的类型安全接口 + * @en Provides type-safe interface decoupled from MongoDB + */ + +// ============================================================================= +// 查询结果 | Query Results +// ============================================================================= + +/** + * @zh 插入结果 + * @en Insert result + */ +export interface InsertOneResult { + insertedId: unknown + acknowledged: boolean +} + +/** + * @zh 批量插入结果 + * @en Insert many result + */ +export interface InsertManyResult { + insertedCount: number + insertedIds: Record + acknowledged: boolean +} + +/** + * @zh 更新结果 + * @en Update result + */ +export interface UpdateResult { + matchedCount: number + modifiedCount: number + upsertedCount: number + upsertedId?: unknown + acknowledged: boolean +} + +/** + * @zh 删除结果 + * @en Delete result + */ +export interface DeleteResult { + deletedCount: number + acknowledged: boolean +} + +// ============================================================================= +// 查询选项 | Query Options +// ============================================================================= + +/** + * @zh 排序方向 + * @en Sort direction + */ +export type SortDirection = 1 | -1 | 'asc' | 'desc' + +/** + * @zh 排序定义 + * @en Sort definition + */ +export type Sort = Record + +/** + * @zh 查找选项 + * @en Find options + */ +export interface FindOptions { + sort?: Sort + limit?: number + skip?: number + projection?: Record +} + +/** + * @zh 查找并更新选项 + * @en Find and update options + */ +export interface FindOneAndUpdateOptions { + returnDocument?: 'before' | 'after' + upsert?: boolean +} + +/** + * @zh 索引选项 + * @en Index options + */ +export interface IndexOptions { + unique?: boolean + sparse?: boolean + expireAfterSeconds?: number + name?: string +} + +// ============================================================================= +// 集合接口 | Collection Interface +// ============================================================================= + +/** + * @zh MongoDB 集合接口 + * @en MongoDB collection interface + * + * @zh 简化的集合操作接口,与 MongoDB 原生类型解耦 + * @en Simplified collection interface, decoupled from MongoDB native types + */ +export interface IMongoCollection { + /** + * @zh 集合名称 + * @en Collection name + */ + readonly name: string + + // ========================================================================= + // 查询 | Query + // ========================================================================= + + /** + * @zh 查找单条记录 + * @en Find one document + */ + findOne(filter: object, options?: FindOptions): Promise + + /** + * @zh 查找多条记录 + * @en Find documents + */ + find(filter: object, options?: FindOptions): Promise + + /** + * @zh 统计记录数 + * @en Count documents + */ + countDocuments(filter?: object): Promise + + // ========================================================================= + // 创建 | Create + // ========================================================================= + + /** + * @zh 插入单条记录 + * @en Insert one document + */ + insertOne(doc: T): Promise + + /** + * @zh 批量插入 + * @en Insert many documents + */ + insertMany(docs: T[]): Promise + + // ========================================================================= + // 更新 | Update + // ========================================================================= + + /** + * @zh 更新单条记录 + * @en Update one document + */ + updateOne(filter: object, update: object): Promise + + /** + * @zh 批量更新 + * @en Update many documents + */ + updateMany(filter: object, update: object): Promise + + /** + * @zh 查找并更新 + * @en Find one and update + */ + findOneAndUpdate( + filter: object, + update: object, + options?: FindOneAndUpdateOptions + ): Promise + + // ========================================================================= + // 删除 | Delete + // ========================================================================= + + /** + * @zh 删除单条记录 + * @en Delete one document + */ + deleteOne(filter: object): Promise + + /** + * @zh 批量删除 + * @en Delete many documents + */ + deleteMany(filter: object): Promise + + // ========================================================================= + // 索引 | Index + // ========================================================================= + + /** + * @zh 创建索引 + * @en Create index + */ + createIndex(spec: Record, options?: IndexOptions): Promise +} + +/** + * @zh MongoDB 数据库接口 + * @en MongoDB database interface + */ +export interface IMongoDatabase { + /** + * @zh 数据库名称 + * @en Database name + */ + readonly name: string + + /** + * @zh 获取集合 + * @en Get collection + */ + collection(name: string): IMongoCollection + + /** + * @zh 列出所有集合 + * @en List all collections + */ + listCollections(): Promise + + /** + * @zh 删除集合 + * @en Drop collection + */ + dropCollection(name: string): Promise +} diff --git a/packages/framework/database-drivers/src/tokens.ts b/packages/framework/database-drivers/src/tokens.ts new file mode 100644 index 00000000..a55663b1 --- /dev/null +++ b/packages/framework/database-drivers/src/tokens.ts @@ -0,0 +1,56 @@ +/** + * @zh 数据库驱动服务令牌 + * @en Database driver service tokens + * + * @zh 用于依赖注入的服务令牌定义 + * @en Service token definitions for dependency injection + */ + +import type { IMongoConnection } from './drivers/MongoConnection.js' +import type { IRedisConnection } from './drivers/RedisConnection.js' + +// ============================================================================= +// 服务令牌类型 | Service Token Type +// ============================================================================= + +/** + * @zh 服务令牌 + * @en Service token + */ +export interface ServiceToken { + readonly id: string + readonly _type?: T +} + +/** + * @zh 创建服务令牌 + * @en Create service token + */ +export function createServiceToken(id: string): ServiceToken { + return { id } +} + +// ============================================================================= +// 连接令牌 | Connection Tokens +// ============================================================================= + +/** + * @zh MongoDB 连接令牌 + * @en MongoDB connection token + * + * @example + * ```typescript + * // 注册 + * services.register(MongoConnectionToken, mongoConnection) + * + * // 获取 + * const mongo = services.get(MongoConnectionToken) + * ``` + */ +export const MongoConnectionToken = createServiceToken('database:mongo') + +/** + * @zh Redis 连接令牌 + * @en Redis connection token + */ +export const RedisConnectionToken = createServiceToken('database:redis') diff --git a/packages/framework/database-drivers/src/types.ts b/packages/framework/database-drivers/src/types.ts new file mode 100644 index 00000000..9187a71f --- /dev/null +++ b/packages/framework/database-drivers/src/types.ts @@ -0,0 +1,338 @@ +/** + * @zh 数据库驱动核心类型定义 + * @en Database driver core type definitions + */ + +// ============================================================================= +// 连接状态 | Connection State +// ============================================================================= + +/** + * @zh 连接状态 + * @en Connection state + */ +export type ConnectionState = + | 'disconnected' // 未连接 | Not connected + | 'connecting' // 连接中 | Connecting + | 'connected' // 已连接 | Connected + | 'disconnecting' // 断开中 | Disconnecting + | 'error' // 错误 | Error + +// ============================================================================= +// 基础连接接口 | Base Connection Interface +// ============================================================================= + +/** + * @zh 数据库连接基础接口 + * @en Base database connection interface + */ +export interface IConnection { + /** + * @zh 连接唯一标识 + * @en Connection unique identifier + */ + readonly id: string + + /** + * @zh 当前连接状态 + * @en Current connection state + */ + readonly state: ConnectionState + + /** + * @zh 建立连接 + * @en Establish connection + */ + connect(): Promise + + /** + * @zh 断开连接 + * @en Disconnect + */ + disconnect(): Promise + + /** + * @zh 检查是否已连接 + * @en Check if connected + */ + isConnected(): boolean + + /** + * @zh 健康检查 + * @en Health check + */ + ping(): Promise +} + +// ============================================================================= +// 连接事件 | Connection Events +// ============================================================================= + +/** + * @zh 连接事件类型 + * @en Connection event types + */ +export type ConnectionEventType = + | 'connected' + | 'disconnected' + | 'error' + | 'reconnecting' + | 'reconnected' + +/** + * @zh 连接事件监听器 + * @en Connection event listener + */ +export type ConnectionEventListener = (event: ConnectionEvent) => void + +/** + * @zh 连接事件 + * @en Connection event + */ +export interface ConnectionEvent { + /** + * @zh 事件类型 + * @en Event type + */ + type: ConnectionEventType + + /** + * @zh 连接 ID + * @en Connection ID + */ + connectionId: string + + /** + * @zh 时间戳 + * @en Timestamp + */ + timestamp: number + + /** + * @zh 错误信息(如果有) + * @en Error message (if any) + */ + error?: Error +} + +/** + * @zh 可监听事件的连接接口 + * @en Connection interface with event support + */ +export interface IEventableConnection extends IConnection { + /** + * @zh 添加事件监听 + * @en Add event listener + */ + on(event: ConnectionEventType, listener: ConnectionEventListener): void + + /** + * @zh 移除事件监听 + * @en Remove event listener + */ + off(event: ConnectionEventType, listener: ConnectionEventListener): void + + /** + * @zh 一次性事件监听 + * @en One-time event listener + */ + once(event: ConnectionEventType, listener: ConnectionEventListener): void +} + +// ============================================================================= +// 连接池配置 | Connection Pool Configuration +// ============================================================================= + +/** + * @zh 连接池配置 + * @en Connection pool configuration + */ +export interface PoolConfig { + /** + * @zh 最小连接数 + * @en Minimum connections + */ + minSize?: number + + /** + * @zh 最大连接数 + * @en Maximum connections + */ + maxSize?: number + + /** + * @zh 获取连接超时时间(毫秒) + * @en Acquire connection timeout in milliseconds + */ + acquireTimeout?: number + + /** + * @zh 空闲连接超时时间(毫秒) + * @en Idle connection timeout in milliseconds + */ + idleTimeout?: number + + /** + * @zh 连接最大生存时间(毫秒) + * @en Maximum connection lifetime in milliseconds + */ + maxLifetime?: number +} + +// ============================================================================= +// 数据库特定配置 | Database Specific Configuration +// ============================================================================= + +/** + * @zh MongoDB 连接配置 + * @en MongoDB connection configuration + */ +export interface MongoConnectionConfig { + /** + * @zh 连接字符串 + * @en Connection string + * + * @example "mongodb://localhost:27017" + * @example "mongodb+srv://user:pass@cluster.mongodb.net" + */ + uri: string + + /** + * @zh 数据库名称 + * @en Database name + */ + database: string + + /** + * @zh 连接池配置 + * @en Pool configuration + */ + pool?: PoolConfig + + /** + * @zh 自动重连 + * @en Auto reconnect + */ + autoReconnect?: boolean + + /** + * @zh 重连间隔(毫秒) + * @en Reconnect interval in milliseconds + */ + reconnectInterval?: number + + /** + * @zh 最大重连次数 + * @en Maximum reconnect attempts + */ + maxReconnectAttempts?: number +} + +/** + * @zh Redis 连接配置 + * @en Redis connection configuration + */ +export interface RedisConnectionConfig { + /** + * @zh 主机地址 + * @en Host address + */ + host?: string + + /** + * @zh 端口 + * @en Port + */ + port?: number + + /** + * @zh 密码 + * @en Password + */ + password?: string + + /** + * @zh 数据库索引 + * @en Database index + */ + db?: number + + /** + * @zh 连接字符串(优先于其他配置) + * @en Connection URL (takes precedence over other options) + */ + url?: string + + /** + * @zh 键前缀 + * @en Key prefix + */ + keyPrefix?: string + + /** + * @zh 自动重连 + * @en Auto reconnect + */ + autoReconnect?: boolean +} + +// ============================================================================= +// 错误类型 | Error Types +// ============================================================================= + +/** + * @zh 数据库错误代码 + * @en Database error codes + */ +export type DatabaseErrorCode = + | 'CONNECTION_FAILED' + | 'CONNECTION_TIMEOUT' + | 'CONNECTION_CLOSED' + | 'AUTHENTICATION_FAILED' + | 'POOL_EXHAUSTED' + | 'QUERY_FAILED' + | 'DUPLICATE_KEY' + | 'NOT_FOUND' + | 'VALIDATION_ERROR' + | 'UNKNOWN' + +/** + * @zh 数据库错误 + * @en Database error + */ +export class DatabaseError extends Error { + constructor( + message: string, + public readonly code: DatabaseErrorCode, + public readonly cause?: Error + ) { + super(message) + this.name = 'DatabaseError' + } +} + +/** + * @zh 连接错误 + * @en Connection error + */ +export class ConnectionError extends DatabaseError { + constructor(message: string, code: DatabaseErrorCode = 'CONNECTION_FAILED', cause?: Error) { + super(message, code, cause) + this.name = 'ConnectionError' + } +} + +/** + * @zh 重复键错误 + * @en Duplicate key error + */ +export class DuplicateKeyError extends DatabaseError { + constructor( + message: string, + public readonly key: string, + cause?: Error + ) { + super(message, 'DUPLICATE_KEY', cause) + this.name = 'DuplicateKeyError' + } +} diff --git a/packages/framework/database-drivers/tsconfig.json b/packages/framework/database-drivers/tsconfig.json new file mode 100644 index 00000000..ebecf242 --- /dev/null +++ b/packages/framework/database-drivers/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declarationDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/framework/database-drivers/tsup.config.ts b/packages/framework/database-drivers/tsup.config.ts new file mode 100644 index 00000000..d1094772 --- /dev/null +++ b/packages/framework/database-drivers/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + external: ['mongodb', 'ioredis'], + treeshake: true, +}); diff --git a/packages/framework/database/module.json b/packages/framework/database/module.json new file mode 100644 index 00000000..06c8536a --- /dev/null +++ b/packages/framework/database/module.json @@ -0,0 +1,23 @@ +{ + "id": "database", + "name": "@esengine/database", + "globalKey": "database", + "displayName": "Database", + "description": "数据库 CRUD 操作和仓库模式,支持用户管理、通用数据存储 | Database CRUD operations and repository pattern with user management and generic data storage", + "version": "1.0.0", + "category": "Infrastructure", + "icon": "Database", + "tags": ["database", "crud", "repository", "user"], + "isCore": false, + "defaultEnabled": true, + "isEngineModule": false, + "canContainContent": false, + "platforms": ["server"], + "dependencies": ["database-drivers"], + "exports": { + "components": [], + "systems": [] + }, + "requiresWasm": false, + "outputPath": "dist/index.js" +} diff --git a/packages/framework/database/package.json b/packages/framework/database/package.json new file mode 100644 index 00000000..0dd1853c --- /dev/null +++ b/packages/framework/database/package.json @@ -0,0 +1,37 @@ +{ + "name": "@esengine/database", + "version": "1.0.0", + "description": "Database CRUD operations and repositories for ESEngine | ESEngine 数据库 CRUD 操作和仓库", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "module.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/database-drivers": "workspace:*" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8.0", + "rimraf": "^5.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/framework/database/src/Repository.ts b/packages/framework/database/src/Repository.ts new file mode 100644 index 00000000..99f96634 --- /dev/null +++ b/packages/framework/database/src/Repository.ts @@ -0,0 +1,313 @@ +/** + * @zh MongoDB 仓库实现 + * @en MongoDB repository implementation + * + * @zh 基于 MongoDB 的通用仓库,支持 CRUD、分页、软删除 + * @en Generic MongoDB repository with CRUD, pagination, and soft delete support + */ + +import { randomUUID } from 'crypto' +import type { IMongoConnection, IMongoCollection } from '@esengine/database-drivers' +import type { + BaseEntity, + IRepository, + PaginatedResult, + PaginationParams, + QueryOptions, + WhereCondition +} from './types.js' + +/** + * @zh MongoDB 仓库基类 + * @en MongoDB repository base class + * + * @example + * ```typescript + * interface Player extends BaseEntity { + * name: string + * score: number + * } + * + * class PlayerRepository extends Repository { + * constructor(connection: IMongoConnection) { + * super(connection, 'players') + * } + * + * async findTopPlayers(limit: number): Promise { + * return this.findMany({ + * sort: { score: 'desc' }, + * limit, + * }) + * } + * } + * ``` + */ +export class Repository implements IRepository { + protected readonly _collection: IMongoCollection + + constructor( + protected readonly connection: IMongoConnection, + public readonly collectionName: string, + protected readonly enableSoftDelete: boolean = false + ) { + this._collection = connection.collection(collectionName) + } + + // ========================================================================= + // 查询 | Query + // ========================================================================= + + async findById(id: string): Promise { + const filter = this._buildFilter({ where: { id } as WhereCondition }) + return this._collection.findOne(filter) + } + + async findOne(options?: QueryOptions): Promise { + const filter = this._buildFilter(options) + const sort = this._buildSort(options) + return this._collection.findOne(filter, { sort }) + } + + async findMany(options?: QueryOptions): Promise { + const filter = this._buildFilter(options) + const sort = this._buildSort(options) + return this._collection.find(filter, { + sort, + skip: options?.offset, + limit: options?.limit + }) + } + + async findPaginated( + pagination: PaginationParams, + options?: Omit, 'limit' | 'offset'> + ): Promise> { + const { page, pageSize } = pagination + const offset = (page - 1) * pageSize + + const [data, total] = await Promise.all([ + this.findMany({ ...options, limit: pageSize, offset }), + this.count(options) + ]) + + const totalPages = Math.ceil(total / pageSize) + + return { + data, + total, + page, + pageSize, + totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + } + + async count(options?: QueryOptions): Promise { + const filter = this._buildFilter(options) + return this._collection.countDocuments(filter) + } + + async exists(options: QueryOptions): Promise { + const count = await this.count({ ...options, limit: 1 }) + return count > 0 + } + + // ========================================================================= + // 创建 | Create + // ========================================================================= + + async create(data: Omit & { id?: string }): Promise { + const now = new Date() + const entity = { + ...data, + id: data.id || randomUUID(), + createdAt: now, + updatedAt: now + } as T + + await this._collection.insertOne(entity) + return entity + } + + async createMany( + data: Array & { id?: string }> + ): Promise { + if (data.length === 0) return [] + + const now = new Date() + const entities = data.map(item => ({ + ...item, + id: item.id || randomUUID(), + createdAt: now, + updatedAt: now + })) as T[] + + await this._collection.insertMany(entities) + return entities + } + + // ========================================================================= + // 更新 | Update + // ========================================================================= + + async update( + id: string, + data: Partial> + ): Promise { + const filter = this._buildFilter({ where: { id } as WhereCondition }) + return this._collection.findOneAndUpdate( + filter, + { $set: { ...data, updatedAt: new Date() } }, + { returnDocument: 'after' } + ) + } + + // ========================================================================= + // 删除 | Delete + // ========================================================================= + + async delete(id: string): Promise { + if (this.enableSoftDelete) { + const result = await this._collection.updateOne( + { id }, + { $set: { deletedAt: new Date(), updatedAt: new Date() } } + ) + return result.modifiedCount > 0 + } + + const result = await this._collection.deleteOne({ id }) + return result.deletedCount > 0 + } + + async deleteMany(options: QueryOptions): Promise { + const filter = this._buildFilter(options) + + if (this.enableSoftDelete) { + const result = await this._collection.updateMany(filter, { + $set: { deletedAt: new Date(), updatedAt: new Date() } + }) + return result.modifiedCount + } + + const result = await this._collection.deleteMany(filter) + return result.deletedCount + } + + // ========================================================================= + // 软删除恢复 | Soft Delete Recovery + // ========================================================================= + + /** + * @zh 恢复软删除的记录 + * @en Restore soft deleted record + */ + async restore(id: string): Promise { + if (!this.enableSoftDelete) { + throw new Error('Soft delete is not enabled for this repository') + } + + return this._collection.findOneAndUpdate( + { id, deletedAt: { $ne: null } }, + { $set: { deletedAt: null, updatedAt: new Date() } }, + { returnDocument: 'after' } + ) + } + + // ========================================================================= + // 内部方法 | Internal Methods + // ========================================================================= + + /** + * @zh 构建过滤条件 + * @en Build filter + */ + protected _buildFilter(options?: QueryOptions): object { + const filter: Record = {} + + if (this.enableSoftDelete && !options?.includeSoftDeleted) { + filter['deletedAt'] = null + } + + if (!options?.where) { + return filter + } + + return { ...filter, ...this._convertWhere(options.where) } + } + + /** + * @zh 转换 where 条件 + * @en Convert where condition + */ + protected _convertWhere(where: WhereCondition): object { + const result: Record = {} + + for (const [key, value] of Object.entries(where)) { + if (key === '$or' && Array.isArray(value)) { + result['$or'] = value.map(v => this._convertWhere(v as WhereCondition)) + continue + } + + if (key === '$and' && Array.isArray(value)) { + result['$and'] = value.map(v => this._convertWhere(v as WhereCondition)) + continue + } + + if (value === undefined) continue + + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + const ops = value as Record + const mongoOps: Record = {} + + if ('$eq' in ops) mongoOps['$eq'] = ops.$eq + if ('$ne' in ops) mongoOps['$ne'] = ops.$ne + if ('$gt' in ops) mongoOps['$gt'] = ops.$gt + if ('$gte' in ops) mongoOps['$gte'] = ops.$gte + if ('$lt' in ops) mongoOps['$lt'] = ops.$lt + if ('$lte' in ops) mongoOps['$lte'] = ops.$lte + if ('$in' in ops) mongoOps['$in'] = ops.$in + if ('$nin' in ops) mongoOps['$nin'] = ops.$nin + if ('$like' in ops) { + const pattern = (ops.$like as string).replace(/%/g, '.*').replace(/_/g, '.') + mongoOps['$regex'] = new RegExp(`^${pattern}$`, 'i') + } + if ('$regex' in ops) { + mongoOps['$regex'] = new RegExp(ops.$regex as string, 'i') + } + + result[key] = Object.keys(mongoOps).length > 0 ? mongoOps : value + } else { + result[key] = value + } + } + + return result + } + + /** + * @zh 构建排序条件 + * @en Build sort condition + */ + protected _buildSort(options?: QueryOptions): Record | undefined { + if (!options?.sort) return undefined + + const result: Record = {} + for (const [key, direction] of Object.entries(options.sort)) { + result[key] = direction === 'desc' ? -1 : 1 + } + return result + } +} + +/** + * @zh 创建仓库实例 + * @en Create repository instance + */ +export function createRepository( + connection: IMongoConnection, + collectionName: string, + enableSoftDelete = false +): Repository { + return new Repository(connection, collectionName, enableSoftDelete) +} diff --git a/packages/framework/database/src/UserRepository.ts b/packages/framework/database/src/UserRepository.ts new file mode 100644 index 00000000..be11bca1 --- /dev/null +++ b/packages/framework/database/src/UserRepository.ts @@ -0,0 +1,335 @@ +/** + * @zh 用户仓库 + * @en User repository + * + * @zh 提供用户管理的常用方法,包括注册、登录、角色管理等 + * @en Provides common user management methods including registration, login, role management + */ + +import type { IMongoConnection } from '@esengine/database-drivers' +import { Repository } from './Repository.js' +import { hashPassword, verifyPassword } from './password.js' +import type { UserEntity } from './types.js' + +/** + * @zh 创建用户参数 + * @en Create user parameters + */ +export interface CreateUserParams { + /** + * @zh 用户名 + * @en Username + */ + username: string + + /** + * @zh 明文密码 + * @en Plain text password + */ + password: string + + /** + * @zh 邮箱 + * @en Email + */ + email?: string + + /** + * @zh 角色列表 + * @en Role list + */ + roles?: string[] + + /** + * @zh 额外数据 + * @en Additional metadata + */ + metadata?: Record +} + +/** + * @zh 用户信息(不含密码) + * @en User info (without password) + */ +export type SafeUser = Omit + +/** + * @zh 用户仓库 + * @en User repository + * + * @example + * ```typescript + * const mongo = createMongoConnection({ uri: '...', database: 'game' }) + * await mongo.connect() + * + * const userRepo = new UserRepository(mongo) + * + * // 注册用户 + * const user = await userRepo.register({ + * username: 'player1', + * password: 'securePassword123', + * email: 'player1@example.com', + * }) + * + * // 验证登录 + * const result = await userRepo.authenticate('player1', 'securePassword123') + * if (result) { + * console.log('登录成功:', result.username) + * } + * ``` + */ +export class UserRepository extends Repository { + constructor(connection: IMongoConnection, collectionName = 'users') { + super(connection, collectionName, true) + } + + // ========================================================================= + // 查询 | Query + // ========================================================================= + + /** + * @zh 根据用户名查找用户 + * @en Find user by username + */ + async findByUsername(username: string): Promise { + return this.findOne({ where: { username } }) + } + + /** + * @zh 根据邮箱查找用户 + * @en Find user by email + */ + async findByEmail(email: string): Promise { + return this.findOne({ where: { email } }) + } + + /** + * @zh 检查用户名是否存在 + * @en Check if username exists + */ + async usernameExists(username: string): Promise { + return this.exists({ where: { username } }) + } + + /** + * @zh 检查邮箱是否存在 + * @en Check if email exists + */ + async emailExists(email: string): Promise { + return this.exists({ where: { email } }) + } + + // ========================================================================= + // 注册与认证 | Registration & Authentication + // ========================================================================= + + /** + * @zh 注册新用户 + * @en Register new user + * + * @param params - @zh 创建用户参数 @en Create user parameters + * @returns @zh 创建的用户(不含密码哈希)@en Created user (without password hash) + * @throws @zh 如果用户名已存在 @en If username already exists + */ + async register(params: CreateUserParams): Promise { + const { username, password, email, roles, metadata } = params + + if (await this.usernameExists(username)) { + throw new Error('Username already exists') + } + + if (email && (await this.emailExists(email))) { + throw new Error('Email already exists') + } + + const passwordHash = await hashPassword(password) + + const user = await this.create({ + username, + passwordHash, + email, + roles: roles ?? ['user'], + isActive: true, + metadata + }) + + return this.toSafeUser(user) + } + + /** + * @zh 验证用户登录 + * @en Authenticate user login + * + * @param username - @zh 用户名 @en Username + * @param password - @zh 明文密码 @en Plain text password + * @returns @zh 验证成功返回用户信息(不含密码),失败返回 null @en Returns user info on success, null on failure + */ + async authenticate(username: string, password: string): Promise { + const user = await this.findByUsername(username) + if (!user || !user.isActive) { + return null + } + + const isValid = await verifyPassword(password, user.passwordHash) + if (!isValid) { + return null + } + + await this.update(user.id, { lastLoginAt: new Date() }) + + return this.toSafeUser(user) + } + + // ========================================================================= + // 密码管理 | Password Management + // ========================================================================= + + /** + * @zh 修改密码 + * @en Change password + * + * @param userId - @zh 用户 ID @en User ID + * @param oldPassword - @zh 旧密码 @en Old password + * @param newPassword - @zh 新密码 @en New password + * @returns @zh 是否修改成功 @en Whether change was successful + */ + async changePassword( + userId: string, + oldPassword: string, + newPassword: string + ): Promise { + const user = await this.findById(userId) + if (!user) { + return false + } + + const isValid = await verifyPassword(oldPassword, user.passwordHash) + if (!isValid) { + return false + } + + const newHash = await hashPassword(newPassword) + const result = await this.update(userId, { passwordHash: newHash }) + return result !== null + } + + /** + * @zh 重置密码(管理员操作) + * @en Reset password (admin operation) + * + * @param userId - @zh 用户 ID @en User ID + * @param newPassword - @zh 新密码 @en New password + */ + async resetPassword(userId: string, newPassword: string): Promise { + const user = await this.findById(userId) + if (!user) { + return false + } + + const newHash = await hashPassword(newPassword) + const result = await this.update(userId, { passwordHash: newHash }) + return result !== null + } + + // ========================================================================= + // 角色管理 | Role Management + // ========================================================================= + + /** + * @zh 添加角色 + * @en Add role to user + */ + async addRole(userId: string, role: string): Promise { + const user = await this.findById(userId) + if (!user) { + return false + } + + const roles = user.roles ?? [] + if (!roles.includes(role)) { + roles.push(role) + await this.update(userId, { roles }) + } + return true + } + + /** + * @zh 移除角色 + * @en Remove role from user + */ + async removeRole(userId: string, role: string): Promise { + const user = await this.findById(userId) + if (!user) { + return false + } + + const roles = (user.roles ?? []).filter(r => r !== role) + await this.update(userId, { roles }) + return true + } + + /** + * @zh 检查用户是否拥有角色 + * @en Check if user has role + */ + async hasRole(userId: string, role: string): Promise { + const user = await this.findById(userId) + return user?.roles?.includes(role) ?? false + } + + /** + * @zh 检查用户是否拥有任一角色 + * @en Check if user has any of the roles + */ + async hasAnyRole(userId: string, roles: string[]): Promise { + const user = await this.findById(userId) + if (!user?.roles) return false + return roles.some(role => user.roles.includes(role)) + } + + // ========================================================================= + // 状态管理 | Status Management + // ========================================================================= + + /** + * @zh 禁用用户 + * @en Deactivate user + */ + async deactivate(userId: string): Promise { + const result = await this.update(userId, { isActive: false }) + return result !== null + } + + /** + * @zh 启用用户 + * @en Activate user + */ + async activate(userId: string): Promise { + const result = await this.update(userId, { isActive: true }) + return result !== null + } + + // ========================================================================= + // 内部方法 | Internal Methods + // ========================================================================= + + /** + * @zh 移除密码哈希 + * @en Remove password hash + */ + private toSafeUser(user: UserEntity): SafeUser { + const { passwordHash, ...safeUser } = user + return safeUser + } +} + +/** + * @zh 创建用户仓库 + * @en Create user repository + */ +export function createUserRepository( + connection: IMongoConnection, + collectionName = 'users' +): UserRepository { + return new UserRepository(connection, collectionName) +} diff --git a/packages/framework/database/src/index.ts b/packages/framework/database/src/index.ts new file mode 100644 index 00000000..88c01396 --- /dev/null +++ b/packages/framework/database/src/index.ts @@ -0,0 +1,152 @@ +/** + * @zh @esengine/database 数据库操作层 + * @en @esengine/database Database Operations Layer + * + * @zh 提供通用的数据库 CRUD 操作、仓库模式、用户管理等功能 + * @en Provides generic database CRUD operations, repository pattern, user management + * + * @example + * ```typescript + * import { createMongoConnection } from '@esengine/database-drivers' + * import { + * Repository, + * UserRepository, + * createUserRepository, + * hashPassword, + * verifyPassword, + * } from '@esengine/database' + * + * // 1. 创建连接(来自 database-drivers) + * const mongo = createMongoConnection({ + * uri: 'mongodb://localhost:27017', + * database: 'game', + * }) + * await mongo.connect() + * + * // 2. 使用用户仓库 + * const userRepo = createUserRepository(mongo) + * + * // 注册 + * const user = await userRepo.register({ + * username: 'player1', + * password: 'securePassword123', + * }) + * + * // 登录 + * const authUser = await userRepo.authenticate('player1', 'securePassword123') + * + * // 3. 自定义仓库 + * interface Player extends BaseEntity { + * name: string + * score: number + * level: number + * } + * + * class PlayerRepository extends Repository { + * constructor(connection: IMongoConnection) { + * super(connection, 'players') + * } + * + * async findTopPlayers(limit = 10): Promise { + * return this.findMany({ + * sort: { score: 'desc' }, + * limit, + * }) + * } + * + * async addScore(playerId: string, points: number): Promise { + * const player = await this.findById(playerId) + * if (!player) return null + * return this.update(playerId, { score: player.score + points }) + * } + * } + * + * // 4. 分页查询 + * const result = await userRepo.findPaginated( + * { page: 1, pageSize: 20 }, + * { where: { isActive: true }, sort: { createdAt: 'desc' } } + * ) + * console.log(`第 ${result.page}/${result.totalPages} 页,共 ${result.total} 条`) + * ``` + */ + +// ============================================================================= +// Types | 类型 +// ============================================================================= + +export type { + BaseEntity, + SoftDeleteEntity, + ComparisonOperators, + WhereCondition, + SortDirection, + SortCondition, + QueryOptions, + PaginationParams, + PaginatedResult, + IRepository, + UserEntity +} from './types.js' + +// ============================================================================= +// Repository | 仓库 +// ============================================================================= + +export { Repository, createRepository } from './Repository.js' + +// ============================================================================= +// User Repository | 用户仓库 +// ============================================================================= + +export { + UserRepository, + createUserRepository, + type CreateUserParams, + type SafeUser +} from './UserRepository.js' + +// ============================================================================= +// Password | 密码工具 +// ============================================================================= + +export { + hashPassword, + verifyPassword, + checkPasswordStrength, + type PasswordHashConfig, + type PasswordStrength, + type PasswordStrengthResult +} from './password.js' + +// ============================================================================= +// Tokens | 服务令牌 +// ============================================================================= + +export { + MongoConnectionToken, + RedisConnectionToken, + UserRepositoryToken, + createServiceToken, + type ServiceToken +} from './tokens.js' + +// ============================================================================= +// Re-exports from database-drivers | 从 database-drivers 重新导出 +// ============================================================================= + +export type { + IMongoConnection, + IRedisConnection, + MongoConnectionConfig, + RedisConnectionConfig, + ConnectionState, + DatabaseErrorCode +} from '@esengine/database-drivers' + +export { + createMongoConnection, + createRedisConnection, + DatabaseError, + ConnectionError, + DuplicateKeyError +} from '@esengine/database-drivers' diff --git a/packages/framework/database/src/password.ts b/packages/framework/database/src/password.ts new file mode 100644 index 00000000..c0a10852 --- /dev/null +++ b/packages/framework/database/src/password.ts @@ -0,0 +1,189 @@ +/** + * @zh 密码加密工具 + * @en Password hashing utilities + * + * @zh 使用 Node.js 内置的 crypto 模块实现安全的密码哈希 + * @en Uses Node.js built-in crypto module for secure password hashing + */ + +import { randomBytes, scrypt, timingSafeEqual } from 'crypto' +import { promisify } from 'util' + +const scryptAsync = promisify(scrypt) + +/** + * @zh 密码哈希配置 + * @en Password hash configuration + */ +export interface PasswordHashConfig { + /** + * @zh 盐的字节长度(默认 16) + * @en Salt length in bytes (default 16) + */ + saltLength?: number + + /** + * @zh scrypt 密钥长度(默认 64) + * @en scrypt key length (default 64) + */ + keyLength?: number +} + +const DEFAULT_CONFIG: Required = { + saltLength: 16, + keyLength: 64 +} + +/** + * @zh 对密码进行哈希处理 + * @en Hash a password + * + * @param password - @zh 明文密码 @en Plain text password + * @param config - @zh 哈希配置 @en Hash configuration + * @returns @zh 格式为 "salt:hash" 的哈希字符串 @en Hash string in "salt:hash" format + * + * @example + * ```typescript + * const hashedPassword = await hashPassword('myPassword123') + * // 存储 hashedPassword 到数据库 + * ``` + */ +export async function hashPassword( + password: string, + config?: PasswordHashConfig +): Promise { + const { saltLength, keyLength } = { ...DEFAULT_CONFIG, ...config } + + const salt = randomBytes(saltLength).toString('hex') + const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer + + return `${salt}:${derivedKey.toString('hex')}` +} + +/** + * @zh 验证密码是否正确 + * @en Verify if a password is correct + * + * @param password - @zh 明文密码 @en Plain text password + * @param hashedPassword - @zh 存储的哈希密码 @en Stored hashed password + * @param config - @zh 哈希配置 @en Hash configuration + * @returns @zh 密码是否匹配 @en Whether the password matches + * + * @example + * ```typescript + * const isValid = await verifyPassword('myPassword123', storedHash) + * if (isValid) { + * // 登录成功 + * } + * ``` + */ +export async function verifyPassword( + password: string, + hashedPassword: string, + config?: PasswordHashConfig +): Promise { + const { keyLength } = { ...DEFAULT_CONFIG, ...config } + + const [salt, storedHash] = hashedPassword.split(':') + if (!salt || !storedHash) { + return false + } + + try { + const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer + const storedBuffer = Buffer.from(storedHash, 'hex') + + return timingSafeEqual(derivedKey, storedBuffer) + } catch { + return false + } +} + +/** + * @zh 密码强度等级 + * @en Password strength level + */ +export type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong' + +/** + * @zh 密码强度检查结果 + * @en Password strength check result + */ +export interface PasswordStrengthResult { + /** + * @zh 强度分数 (0-6) + * @en Strength score (0-6) + */ + score: number + + /** + * @zh 强度等级 + * @en Strength level + */ + level: PasswordStrength + + /** + * @zh 改进建议 + * @en Improvement suggestions + */ + feedback: string[] +} + +/** + * @zh 检查密码强度 + * @en Check password strength + * + * @param password - @zh 明文密码 @en Plain text password + * @returns @zh 密码强度信息 @en Password strength information + */ +export function checkPasswordStrength(password: string): PasswordStrengthResult { + const feedback: string[] = [] + let score = 0 + + if (password.length >= 8) { + score += 1 + } else { + feedback.push('Password should be at least 8 characters') + } + + if (password.length >= 12) { + score += 1 + } + + if (/[a-z]/.test(password)) { + score += 1 + } else { + feedback.push('Password should contain lowercase letters') + } + + if (/[A-Z]/.test(password)) { + score += 1 + } else { + feedback.push('Password should contain uppercase letters') + } + + if (/[0-9]/.test(password)) { + score += 1 + } else { + feedback.push('Password should contain numbers') + } + + if (/[^a-zA-Z0-9]/.test(password)) { + score += 1 + } else { + feedback.push('Password should contain special characters') + } + + let level: PasswordStrength + if (score <= 2) { + level = 'weak' + } else if (score <= 3) { + level = 'fair' + } else if (score <= 4) { + level = 'good' + } else { + level = 'strong' + } + + return { score, level, feedback } +} diff --git a/packages/framework/database/src/tokens.ts b/packages/framework/database/src/tokens.ts new file mode 100644 index 00000000..5c88ba53 --- /dev/null +++ b/packages/framework/database/src/tokens.ts @@ -0,0 +1,17 @@ +/** + * @zh 数据库服务令牌 + * @en Database service tokens + */ + +import type { ServiceToken, createServiceToken as createToken } from '@esengine/database-drivers' +import type { UserRepository } from './UserRepository.js' + +// Re-export from database-drivers for convenience +export { MongoConnectionToken, RedisConnectionToken, createServiceToken } from '@esengine/database-drivers' +export type { ServiceToken } from '@esengine/database-drivers' + +/** + * @zh 用户仓库令牌 + * @en User repository token + */ +export const UserRepositoryToken: ServiceToken = { id: 'database:userRepository' } diff --git a/packages/framework/database/src/types.ts b/packages/framework/database/src/types.ts new file mode 100644 index 00000000..a6dc64fa --- /dev/null +++ b/packages/framework/database/src/types.ts @@ -0,0 +1,333 @@ +/** + * @zh 数据库核心类型定义 + * @en Database core type definitions + */ + +// ============================================================================= +// 实体类型 | Entity Types +// ============================================================================= + +/** + * @zh 基础实体接口 + * @en Base entity interface + */ +export interface BaseEntity { + /** + * @zh 实体唯一标识 + * @en Entity unique identifier + */ + id: string + + /** + * @zh 创建时间 + * @en Creation timestamp + */ + createdAt?: Date + + /** + * @zh 更新时间 + * @en Update timestamp + */ + updatedAt?: Date +} + +/** + * @zh 软删除实体接口 + * @en Soft delete entity interface + */ +export interface SoftDeleteEntity extends BaseEntity { + /** + * @zh 删除时间(null 表示未删除) + * @en Deletion timestamp (null means not deleted) + */ + deletedAt?: Date | null +} + +// ============================================================================= +// 查询类型 | Query Types +// ============================================================================= + +/** + * @zh 比较操作符 + * @en Comparison operators + */ +export interface ComparisonOperators { + $eq?: T + $ne?: T + $gt?: T + $gte?: T + $lt?: T + $lte?: T + $in?: T[] + $nin?: T[] + $like?: string + $regex?: string +} + +/** + * @zh 查询条件 + * @en Query condition + */ +export type WhereCondition = { + [K in keyof T]?: T[K] | ComparisonOperators +} & { + $or?: WhereCondition[] + $and?: WhereCondition[] +} + +/** + * @zh 排序方向 + * @en Sort direction + */ +export type SortDirection = 'asc' | 'desc' + +/** + * @zh 排序条件 + * @en Sort condition + */ +export type SortCondition = { + [K in keyof T]?: SortDirection +} + +/** + * @zh 查询选项 + * @en Query options + */ +export interface QueryOptions { + /** + * @zh 过滤条件 + * @en Filter conditions + */ + where?: WhereCondition + + /** + * @zh 排序条件 + * @en Sort conditions + */ + sort?: SortCondition + + /** + * @zh 限制返回数量 + * @en Limit number of results + */ + limit?: number + + /** + * @zh 跳过记录数 + * @en Number of records to skip + */ + offset?: number + + /** + * @zh 是否包含软删除记录 + * @en Whether to include soft deleted records + */ + includeSoftDeleted?: boolean +} + +// ============================================================================= +// 分页类型 | Pagination Types +// ============================================================================= + +/** + * @zh 分页参数 + * @en Pagination parameters + */ +export interface PaginationParams { + /** + * @zh 页码(从 1 开始) + * @en Page number (starts from 1) + */ + page: number + + /** + * @zh 每页数量 + * @en Items per page + */ + pageSize: number +} + +/** + * @zh 分页结果 + * @en Pagination result + */ +export interface PaginatedResult { + /** + * @zh 数据列表 + * @en Data list + */ + data: T[] + + /** + * @zh 总记录数 + * @en Total count + */ + total: number + + /** + * @zh 当前页码 + * @en Current page + */ + page: number + + /** + * @zh 每页数量 + * @en Page size + */ + pageSize: number + + /** + * @zh 总页数 + * @en Total pages + */ + totalPages: number + + /** + * @zh 是否有下一页 + * @en Whether has next page + */ + hasNext: boolean + + /** + * @zh 是否有上一页 + * @en Whether has previous page + */ + hasPrev: boolean +} + +// ============================================================================= +// 仓库接口 | Repository Interface +// ============================================================================= + +/** + * @zh 仓库接口 + * @en Repository interface + */ +export interface IRepository { + /** + * @zh 集合名称 + * @en Collection name + */ + readonly collectionName: string + + /** + * @zh 根据 ID 查找 + * @en Find by ID + */ + findById(id: string): Promise + + /** + * @zh 查找单条记录 + * @en Find one record + */ + findOne(options?: QueryOptions): Promise + + /** + * @zh 查找多条记录 + * @en Find many records + */ + findMany(options?: QueryOptions): Promise + + /** + * @zh 分页查询 + * @en Paginated query + */ + findPaginated( + pagination: PaginationParams, + options?: Omit, 'limit' | 'offset'> + ): Promise> + + /** + * @zh 统计记录数 + * @en Count records + */ + count(options?: QueryOptions): Promise + + /** + * @zh 检查记录是否存在 + * @en Check if record exists + */ + exists(options: QueryOptions): Promise + + /** + * @zh 创建记录 + * @en Create record + */ + create(data: Omit & { id?: string }): Promise + + /** + * @zh 批量创建 + * @en Bulk create + */ + createMany(data: Array & { id?: string }>): Promise + + /** + * @zh 更新记录 + * @en Update record + */ + update(id: string, data: Partial>): Promise + + /** + * @zh 删除记录 + * @en Delete record + */ + delete(id: string): Promise + + /** + * @zh 批量删除 + * @en Bulk delete + */ + deleteMany(options: QueryOptions): Promise +} + +// ============================================================================= +// 用户实体 | User Entity +// ============================================================================= + +/** + * @zh 用户实体 + * @en User entity + */ +export interface UserEntity extends SoftDeleteEntity { + /** + * @zh 用户名 + * @en Username + */ + username: string + + /** + * @zh 密码哈希 + * @en Password hash + */ + passwordHash: string + + /** + * @zh 邮箱 + * @en Email + */ + email?: string + + /** + * @zh 用户角色 + * @en User roles + */ + roles: string[] + + /** + * @zh 是否启用 + * @en Is active + */ + isActive: boolean + + /** + * @zh 最后登录时间 + * @en Last login timestamp + */ + lastLoginAt?: Date + + /** + * @zh 额外数据 + * @en Additional metadata + */ + metadata?: Record +} diff --git a/packages/framework/database/tsconfig.json b/packages/framework/database/tsconfig.json new file mode 100644 index 00000000..ebecf242 --- /dev/null +++ b/packages/framework/database/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declarationDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/framework/database/tsup.config.ts b/packages/framework/database/tsup.config.ts new file mode 100644 index 00000000..cc13f818 --- /dev/null +++ b/packages/framework/database/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + external: ['@esengine/database-drivers'], + treeshake: true, +}); diff --git a/packages/framework/transaction/package.json b/packages/framework/transaction/package.json index fbfeca15..66fe77ee 100644 --- a/packages/framework/transaction/package.json +++ b/packages/framework/transaction/package.json @@ -25,7 +25,7 @@ "test:watch": "vitest" }, "dependencies": { - "@esengine/server": "workspace:*" + "@esengine/database-drivers": "workspace:*" }, "peerDependencies": { "ioredis": "^5.3.0", diff --git a/packages/framework/transaction/src/index.ts b/packages/framework/transaction/src/index.ts index be1f7890..4f3b9cde 100644 --- a/packages/framework/transaction/src/index.ts +++ b/packages/framework/transaction/src/index.ts @@ -88,9 +88,7 @@ export { export { MongoStorage, createMongoStorage, - type MongoStorageConfig, - type MongoDb, - type MongoCollection + type MongoStorageConfig } from './storage/MongoStorage.js'; // ============================================================================= diff --git a/packages/framework/transaction/src/storage/MongoStorage.ts b/packages/framework/transaction/src/storage/MongoStorage.ts index 8b73d659..62d014a9 100644 --- a/packages/framework/transaction/src/storage/MongoStorage.ts +++ b/packages/framework/transaction/src/storage/MongoStorage.ts @@ -2,10 +2,11 @@ * @zh MongoDB 存储实现 * @en MongoDB storage implementation * - * @zh 支持持久化事务日志和查询 - * @en Supports persistent transaction logs and queries + * @zh 基于共享连接的事务存储,使用 @esengine/database-drivers 提供的连接 + * @en Transaction storage based on shared connection from @esengine/database-drivers */ +import type { IMongoConnection, IMongoCollection } from '@esengine/database-drivers'; import type { ITransactionStorage, TransactionLog, @@ -13,43 +14,9 @@ import type { OperationLog } from '../core/types.js'; -/** - * @zh MongoDB Collection 接口 - * @en MongoDB Collection interface - */ -export interface MongoCollection { - findOne(filter: object): Promise - find(filter: object): { - toArray(): Promise - } - insertOne(doc: T): Promise<{ insertedId: unknown }> - updateOne(filter: object, update: object): Promise<{ modifiedCount: number }> - deleteOne(filter: object): Promise<{ deletedCount: number }> - createIndex(spec: object, options?: object): Promise -} - -/** - * @zh MongoDB 数据库接口 - * @en MongoDB database interface - */ -export interface MongoDb { - collection(name: string): MongoCollection -} - -/** - * @zh MongoDB 客户端接口 - * @en MongoDB client interface - */ -export interface MongoClient { - db(name?: string): MongoDb - close(): Promise -} - -/** - * @zh MongoDB 连接工厂 - * @en MongoDB connection factory - */ -export type MongoClientFactory = () => MongoClient | Promise +// ============================================================================= +// 配置类型 | Configuration Types +// ============================================================================= /** * @zh MongoDB 存储配置 @@ -57,29 +24,10 @@ export type MongoClientFactory = () => MongoClient | Promise */ export interface MongoStorageConfig { /** - * @zh MongoDB 客户端工厂(惰性连接) - * @en MongoDB client factory (lazy connection) - * - * @example - * ```typescript - * import { MongoClient } from 'mongodb' - * const storage = new MongoStorage({ - * factory: async () => { - * const client = new MongoClient('mongodb://localhost:27017') - * await client.connect() - * return client - * }, - * database: 'game' - * }) - * ``` + * @zh MongoDB 连接(来自 @esengine/database-drivers) + * @en MongoDB connection (from @esengine/database-drivers) */ - factory: MongoClientFactory - - /** - * @zh 数据库名称 - * @en Database name - */ - database: string + connection: IMongoConnection /** * @zh 事务日志集合名称 @@ -100,6 +48,10 @@ export interface MongoStorageConfig { lockCollection?: string } +// ============================================================================= +// 内部类型 | Internal Types +// ============================================================================= + interface LockDocument { _id: string token: string @@ -112,50 +64,40 @@ interface DataDocument { expireAt?: Date } +// ============================================================================= +// 实现 | Implementation +// ============================================================================= + /** * @zh MongoDB 存储 * @en MongoDB storage * - * @zh 基于 MongoDB 的事务存储,支持持久化、复杂查询和惰性连接 - * @en MongoDB-based transaction storage with persistence, complex queries and lazy connection + * @zh 基于 MongoDB 的事务存储,使用 @esengine/database-drivers 的共享连接 + * @en MongoDB-based transaction storage using shared connection from @esengine/database-drivers * * @example * ```typescript - * import { MongoClient } from 'mongodb' + * import { createMongoConnection } from '@esengine/database-drivers' + * import { MongoStorage } from '@esengine/transaction' * - * // 创建存储(惰性连接,首次操作时才连接) - * const storage = new MongoStorage({ - * factory: async () => { - * const client = new MongoClient('mongodb://localhost:27017') - * await client.connect() - * return client - * }, + * const mongo = createMongoConnection({ + * uri: 'mongodb://localhost:27017', * database: 'game' * }) + * await mongo.connect() * - * await storage.ensureIndexes() - * - * // 使用后手动关闭 - * await storage.close() - * - * // 或使用 await using 自动关闭 (TypeScript 5.2+) - * await using storage = new MongoStorage({ ... }) - * // 作用域结束时自动关闭 + * const storage = new MongoStorage({ connection: mongo }) * ``` */ export class MongoStorage implements ITransactionStorage { - private _client: MongoClient | null = null; - private _db: MongoDb | null = null; - private _factory: MongoClientFactory; - private _database: string; - private _transactionCollection: string; - private _dataCollection: string; - private _lockCollection: string; + private readonly _connection: IMongoConnection; + private readonly _transactionCollection: string; + private readonly _dataCollection: string; + private readonly _lockCollection: string; private _closed: boolean = false; constructor(config: MongoStorageConfig) { - this._factory = config.factory; - this._database = config.database; + this._connection = config.connection; this._transactionCollection = config.transactionCollection ?? 'transactions'; this._dataCollection = config.dataCollection ?? 'transaction_data'; this._lockCollection = config.lockCollection ?? 'transaction_locks'; @@ -166,36 +108,30 @@ export class MongoStorage implements ITransactionStorage { // ========================================================================= /** - * @zh 获取数据库实例(惰性连接) - * @en Get database instance (lazy connection) + * @zh 获取集合 + * @en Get collection */ - private async _getDb(): Promise { + private _getCollection(name: string): IMongoCollection { if (this._closed) { throw new Error('MongoStorage is closed'); } - if (!this._db) { - this._client = await this._factory(); - this._db = this._client.db(this._database); + if (!this._connection.isConnected()) { + throw new Error('MongoDB connection is not connected'); } - return this._db; + return this._connection.collection(name); } /** - * @zh 关闭存储连接 - * @en Close storage connection + * @zh 关闭存储 + * @en Close storage + * + * @zh 不会关闭共享连接,只标记存储为已关闭 + * @en Does not close shared connection, only marks storage as closed */ async close(): Promise { - if (this._closed) return; - this._closed = true; - - if (this._client) { - await this._client.close(); - this._client = null; - this._db = null; - } } /** @@ -211,16 +147,15 @@ export class MongoStorage implements ITransactionStorage { * @en Ensure indexes exist */ async ensureIndexes(): Promise { - const db = await this._getDb(); - const txColl = db.collection(this._transactionCollection); + const txColl = this._getCollection(this._transactionCollection); await txColl.createIndex({ state: 1 }); await txColl.createIndex({ 'metadata.serverId': 1 }); await txColl.createIndex({ createdAt: 1 }); - const lockColl = db.collection(this._lockCollection); + const lockColl = this._getCollection(this._lockCollection); await lockColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }); - const dataColl = db.collection(this._dataCollection); + const dataColl = this._getCollection(this._dataCollection); await dataColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 }); } @@ -229,19 +164,14 @@ export class MongoStorage implements ITransactionStorage { // ========================================================================= async acquireLock(key: string, ttl: number): Promise { - const db = await this._getDb(); - const coll = db.collection(this._lockCollection); + const coll = this._getCollection(this._lockCollection); const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`; const expireAt = new Date(Date.now() + ttl); try { - await coll.insertOne({ - _id: key, - token, - expireAt - }); + await coll.insertOne({ _id: key, token, expireAt } as LockDocument); return token; - } catch (error) { + } catch { const existing = await coll.findOne({ _id: key }); if (existing && existing.expireAt < new Date()) { const result = await coll.updateOne( @@ -257,8 +187,7 @@ export class MongoStorage implements ITransactionStorage { } async releaseLock(key: string, token: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._lockCollection); + const coll = this._getCollection(this._lockCollection); const result = await coll.deleteOne({ _id: key, token }); return result.deletedCount > 0; } @@ -268,8 +197,7 @@ export class MongoStorage implements ITransactionStorage { // ========================================================================= async saveTransaction(tx: TransactionLog): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); const existing = await coll.findOne({ _id: tx.id }); if (existing) { @@ -278,13 +206,12 @@ export class MongoStorage implements ITransactionStorage { { $set: { ...tx, _id: tx.id } } ); } else { - await coll.insertOne({ ...tx, _id: tx.id }); + await coll.insertOne({ ...tx, _id: tx.id } as TransactionLog & { _id: string }); } } async getTransaction(id: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); const doc = await coll.findOne({ _id: id }); if (!doc) return null; @@ -294,8 +221,7 @@ export class MongoStorage implements ITransactionStorage { } async updateTransactionState(id: string, state: TransactionState): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); await coll.updateOne( { _id: id }, { $set: { state, updatedAt: Date.now() } } @@ -308,8 +234,7 @@ export class MongoStorage implements ITransactionStorage { state: OperationLog['state'], error?: string ): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); const update: Record = { [`operations.${operationIndex}.state`]: state, @@ -333,8 +258,7 @@ export class MongoStorage implements ITransactionStorage { } async getPendingTransactions(serverId?: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); const filter: Record = { state: { $in: ['pending', 'executing'] } @@ -344,13 +268,12 @@ export class MongoStorage implements ITransactionStorage { filter['metadata.serverId'] = serverId; } - const docs = await coll.find(filter).toArray(); + const docs = await coll.find(filter); return docs.map(({ _id, ...tx }) => tx as TransactionLog); } async deleteTransaction(id: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._transactionCollection); + const coll = this._getCollection(this._transactionCollection); await coll.deleteOne({ _id: id }); } @@ -359,8 +282,7 @@ export class MongoStorage implements ITransactionStorage { // ========================================================================= async get(key: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._dataCollection); + const coll = this._getCollection(this._dataCollection); const doc = await coll.findOne({ _id: key }); if (!doc) return null; @@ -374,13 +296,9 @@ export class MongoStorage implements ITransactionStorage { } async set(key: string, value: T, ttl?: number): Promise { - const db = await this._getDb(); - const coll = db.collection(this._dataCollection); + const coll = this._getCollection(this._dataCollection); - const doc: DataDocument = { - _id: key, - value - }; + const doc: DataDocument = { _id: key, value }; if (ttl) { doc.expireAt = new Date(Date.now() + ttl); @@ -395,8 +313,7 @@ export class MongoStorage implements ITransactionStorage { } async delete(key: string): Promise { - const db = await this._getDb(); - const coll = db.collection(this._dataCollection); + const coll = this._getCollection(this._dataCollection); const result = await coll.deleteOne({ _id: key }); return result.deletedCount > 0; } @@ -405,7 +322,24 @@ export class MongoStorage implements ITransactionStorage { /** * @zh 创建 MongoDB 存储 * @en Create MongoDB storage + * + * @example + * ```typescript + * import { createMongoConnection } from '@esengine/database-drivers' + * import { createMongoStorage } from '@esengine/transaction' + * + * const mongo = createMongoConnection({ + * uri: 'mongodb://localhost:27017', + * database: 'game' + * }) + * await mongo.connect() + * + * const storage = createMongoStorage(mongo) + * ``` */ -export function createMongoStorage(config: MongoStorageConfig): MongoStorage { - return new MongoStorage(config); +export function createMongoStorage( + connection: IMongoConnection, + options?: Omit +): MongoStorage { + return new MongoStorage({ connection, ...options }); } diff --git a/packages/framework/transaction/src/storage/index.ts b/packages/framework/transaction/src/storage/index.ts index 6a816c3c..68551c7d 100644 --- a/packages/framework/transaction/src/storage/index.ts +++ b/packages/framework/transaction/src/storage/index.ts @@ -5,4 +5,4 @@ export { MemoryStorage, createMemoryStorage, type MemoryStorageConfig } from './MemoryStorage.js'; export { RedisStorage, createRedisStorage, type RedisStorageConfig, type RedisClient } from './RedisStorage.js'; -export { MongoStorage, createMongoStorage, type MongoStorageConfig, type MongoDb, type MongoCollection } from './MongoStorage.js'; +export { MongoStorage, createMongoStorage, type MongoStorageConfig } from './MongoStorage.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8223a265..c328ce1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1506,6 +1506,46 @@ importers: version: 8.50.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) publishDirectory: dist + packages/framework/database: + dependencies: + '@esengine/database-drivers': + specifier: workspace:* + version: link:../database-drivers + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.27 + rimraf: + specifier: ^5.0.0 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@20.19.27))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + + packages/framework/database-drivers: + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.27 + ioredis: + specifier: ^5.3.0 + version: 5.8.2 + mongodb: + specifier: ^6.12.0 + version: 6.21.0(socks@2.8.7) + rimraf: + specifier: ^5.0.0 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@20.19.27))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + packages/framework/fsm: dependencies: tslib: @@ -1786,9 +1826,9 @@ importers: packages/framework/transaction: dependencies: - '@esengine/server': + '@esengine/database-drivers': specifier: workspace:* - version: link:../server + version: link:../database-drivers ioredis: specifier: ^5.3.0 version: 5.8.2