Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions[bot]
0f5aa633d8 chore: release packages (#413)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 18:12:40 +08:00
YHH
85171a0a5c fix(database): include dist directory in npm package (#412)
* fix(database): include dist directory in npm package

* fix(ci): add database packages to release build
2025-12-31 18:10:40 +08:00
github-actions[bot]
35d81880a7 chore: release packages (#411)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 16:33:05 +08:00
YHH
71022abc99 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
2025-12-31 16:26:53 +08:00
github-actions[bot]
87f71e2251 chore: release packages (#409)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 14:32:18 +08:00
YHH
b9ea8d14cf feat(behavior-tree): add action() and condition() methods to BehaviorTreeBuilder (#408)
- Add action(implementationType, name?, config?) for custom action executors
- Add condition(implementationType, name?, config?) for custom condition executors
- Update documentation (EN and CN) with usage examples
- Add test script to package.json
2025-12-31 14:30:31 +08:00
yhh
10d0fb1d5c fix(rapier2d): fix external config path mismatch in tsup 2025-12-31 13:25:30 +08:00
github-actions[bot]
71e111415f chore: release packages (#407)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 12:18:18 +08:00
YHH
0de45279e6 fix(behavior-tree): export NodeExecutorMetadata as value instead of type (#406) 2025-12-31 12:16:17 +08:00
51 changed files with 5646 additions and 193 deletions

View File

@@ -57,6 +57,9 @@ jobs:
pnpm --filter "@esengine/rpc" build
pnpm --filter "@esengine/network" build
pnpm --filter "@esengine/server" build
pnpm --filter "@esengine/database-drivers" build
pnpm --filter "@esengine/database" build
pnpm --filter "@esengine/transaction" build
pnpm --filter "@esengine/cli" build
pnpm --filter "create-esengine-server" build

View File

@@ -182,6 +182,70 @@ export class IsHealthLow implements INodeExecutor {
}
```
## Using Custom Executors in BehaviorTreeBuilder
After defining a custom executor with `@NodeExecutorMetadata`, use the `.action()` method in the builder:
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// Use custom executor in behavior tree
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// Use custom action - matches implementationType in decorator
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToTarget', 'Chase')
.end()
.action('WaitAction', 'Idle', { duration: 1000 })
.end()
.build();
// Start the behavior tree
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### Builder Methods for Custom Nodes
| Method | Description |
|--------|-------------|
| `.action(type, name?, config?)` | Add custom action node |
| `.condition(type, name?, config?)` | Add custom condition node |
| `.executeAction(name)` | Use blackboard function `action_{name}` |
| `.executeCondition(name)` | Use blackboard function `condition_{name}` |
### Complete Example
```typescript
// 1. Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`Attacking with ${damage} damage!`);
return TaskStatus.Success;
}
}
// 2. Build and use
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## Registering Custom Executors
Executors are auto-registered via the decorator. To manually register:

View File

@@ -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<T> │ ← 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<User>('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

View File

@@ -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<Player>(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<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
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<Player>(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

View File

@@ -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:

View File

@@ -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

View File

@@ -606,6 +606,107 @@ export class RetryDecorator implements INodeExecutor {
}
```
## 在代码中使用自定义执行器
定义了自定义执行器后,可以通过 `BehaviorTreeBuilder``.action()``.condition()` 方法在代码中使用:
### 使用 action() 方法
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// 使用自定义执行器构建行为树
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// 使用自定义动作 - implementationType 匹配装饰器中的定义
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToPosition', 'Chase', { speed: 10 })
.end()
.action('DelayAction', 'Idle', { duration: 1.0 })
.end()
.build();
// 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### 使用 condition() 方法
```typescript
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.sequence('AttackBranch')
// 使用自定义条件
.condition('CheckHealth', 'IsHealthy', { threshold: 50, operator: 'greater' })
.action('AttackAction', 'Attack')
.end()
.end()
.build();
```
### Builder 方法对照表
| 方法 | 说明 | 使用场景 |
|------|------|----------|
| `.action(type, name?, config?)` | 使用自定义动作执行器 | 自定义 Action 类 |
| `.condition(type, name?, config?)` | 使用自定义条件执行器 | 自定义 Condition 类 |
| `.executeAction(name)` | 调用黑板函数 `action_{name}` | 简单逻辑、快速原型 |
| `.executeCondition(name)` | 调用黑板函数 `condition_{name}` | 简单条件判断 |
### 完整示例
```typescript
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
NodeExecutorMetadata,
INodeExecutor,
NodeExecutionContext,
TaskStatus,
NodeType,
BindingHelper
} from '@esengine/behavior-tree';
// 1. 定义自定义执行器
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: '攻击',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`执行攻击,造成 ${damage} 点伤害!`);
return TaskStatus.Success;
}
}
// 2. 构建行为树
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('MainBehavior')
.sequence('AttackBranch')
.condition('CheckHealth', 'HasEnoughHealth', { threshold: 20, operator: 'greater' })
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.log('逃跑', 'Flee')
.end()
.build();
// 3. 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, enemyAI);
```
## 注册执行器
### 自动注册

View File

@@ -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<T> │ ← 类型安全接口 │
│ │ (适配器模式) │ 与 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<User>('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/) - 依赖注入集成

View File

@@ -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<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取类型化集合 */
collection<T extends object>(name: string): IMongoCollection<T>
/** 获取数据库接口 */
getDatabase(): IMongoDatabase
/** 获取原生客户端(高级用法) */
getNativeClient(): MongoClientType
/** 获取原生数据库(高级用法) */
getNativeDatabase(): Db
}
```
## IMongoCollection 接口
类型安全的集合接口,与原生 MongoDB 类型解耦:
```typescript
interface IMongoCollection<T extends object> {
readonly name: string
// 查询
findOne(filter: object, options?: FindOptions): Promise<T | null>
find(filter: object, options?: FindOptions): Promise<T[]>
countDocuments(filter?: object): Promise<number>
// 插入
insertOne(doc: T): Promise<InsertOneResult>
insertMany(docs: T[]): Promise<InsertManyResult>
// 更新
updateOne(filter: object, update: object): Promise<UpdateResult>
updateMany(filter: object, update: object): Promise<UpdateResult>
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// 删除
deleteOne(filter: object): Promise<DeleteResult>
deleteMany(filter: object): Promise<DeleteResult>
// 索引
createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string>
}
```
## 使用示例
### 基本 CRUD
```typescript
interface User {
id: string
name: string
email: string
score: number
}
const users = mongo.collection<User>('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<Player>(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 })
```

View File

@@ -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<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取值 */
get(key: string): Promise<string | null>
/** 设置值(可选 TTL单位秒 */
set(key: string, value: string, ttl?: number): Promise<void>
/** 删除键 */
del(key: string): Promise<boolean>
/** 检查键是否存在 */
exists(key: string): Promise<boolean>
/** 设置过期时间(秒) */
expire(key: string, seconds: number): Promise<boolean>
/** 获取剩余过期时间(秒) */
ttl(key: string): Promise<number>
/** 获取原生客户端(高级用法) */
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` | 发生错误 |

View File

@@ -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<Player>(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<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
return this.findMany({
where: { rank }
})
}
async incrementScore(playerId: string, amount: number): Promise<Player | null> {
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/) - 查询条件语法

View File

@@ -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$' } }
]
}
})
```

View File

@@ -0,0 +1,244 @@
---
title: "Repository API"
description: "泛型仓库接口CRUD 操作、分页、软删除"
---
## 创建仓库
### 使用工厂函数
```typescript
import { createRepository } from '@esengine/database'
const playerRepo = createRepository<Player>(mongo, 'players')
// 启用软删除
const playerRepo = createRepository<Player>(mongo, 'players', true)
```
### 继承 Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
interface Player extends BaseEntity {
name: string
score: number
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players', false) // 第三个参数:启用软删除
}
// 添加自定义方法
async findTopPlayers(limit: number): Promise<Player[]> {
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<Player>(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<T> {
/** 查询条件 */
where?: WhereCondition<T>
/** 排序 */
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
/** 限制数量 */
limit?: number
/** 偏移量 */
offset?: number
/** 包含软删除记录(仅在启用软删除时有效) */
includeSoftDeleted?: boolean
}
```
## PaginatedResult
```typescript
interface PaginatedResult<T> {
data: T[]
total: number
page: number
pageSize: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
```

View File

@@ -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<UserEntity, 'passwordHash'>
```
### 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<GameUser | null> {
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<GameUser | null>
}
async findTopPlayers(limit: number = 10): Promise<GameUser[]> {
return this.findMany({
sort: { level: 'desc', experience: 'desc' },
limit
}) as Promise<GameUser[]>
}
}
```

View File

@@ -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 模式数据操作 |
## 安装
所有模块都可以独立安装:

View File

@@ -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();
```
### 特点

View File

@@ -1,5 +1,54 @@
# @esengine/behavior-tree
## 4.2.0
### Minor Changes
- [#408](https://github.com/esengine/esengine/pull/408) [`b9ea8d1`](https://github.com/esengine/esengine/commit/b9ea8d14cf38e1480f638c229f9ee150b65f0c60) Thanks [@esengine](https://github.com/esengine)! - feat: add action() and condition() methods to BehaviorTreeBuilder
Added new methods to support custom executor types directly in the builder:
- `action(implementationType, name?, config?)` - Use custom action executors registered via `@NodeExecutorMetadata`
- `condition(implementationType, name?, config?)` - Use custom condition executors
This provides a cleaner API for using custom node executors compared to the existing `executeAction()` which only supports blackboard functions.
Example:
```typescript
// Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat'
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
return TaskStatus.Success;
}
}
// Use in builder
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## 4.1.2
### Patch Changes
- [#406](https://github.com/esengine/esengine/pull/406) [`0de4527`](https://github.com/esengine/esengine/commit/0de45279e612c04ae9be7fbd65ce496e4797a43c) Thanks [@esengine](https://github.com/esengine)! - fix(behavior-tree): export NodeExecutorMetadata as value instead of type
Fixed the export of `NodeExecutorMetadata` decorator in `execution/index.ts`.
Previously it was exported as `export type { NodeExecutorMetadata }` which only
exported the type signature, not the actual function. This caused runtime errors
in Cocos Creator: "TypeError: (intermediate value) is not a function".
Changed to `export { NodeExecutorMetadata }` to properly export the decorator function.
## 4.1.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/behavior-tree",
"version": "4.1.1",
"version": "4.2.0",
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -29,7 +29,8 @@
"clean": "rimraf dist tsconfig.tsbuildinfo",
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"author": "yhh",
"license": "MIT",

View File

@@ -181,12 +181,73 @@ export class BehaviorTreeBuilder {
}
/**
* 添加执行动作
* 添加执行动作(通过黑板函数)
*
* @zh 使用黑板中的 action_{actionName} 函数执行动作
* @en Execute action using action_{actionName} function from blackboard
*
* @example
* ```typescript
* BehaviorTreeBuilder.create("AI")
* .defineBlackboardVariable("action_Attack", (entity) => TaskStatus.Success)
* .selector("Root")
* .executeAction("Attack")
* .end()
* .build();
* ```
*/
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
}
/**
* 添加自定义动作节点
*
* @zh 直接使用注册的执行器类型(通过 @NodeExecutorMetadata 装饰器注册的类)
* @en Use a registered executor type directly (class registered via @NodeExecutorMetadata decorator)
*
* @param implementationType - 执行器类型名称(@NodeExecutorMetadata 中的 implementationType
* @param name - 节点显示名称
* @param config - 节点配置参数
*
* @example
* ```typescript
* // 1. 定义自定义执行器
* @NodeExecutorMetadata({
* implementationType: 'AttackAction',
* nodeType: NodeType.Action,
* displayName: '攻击动作',
* category: 'Action'
* })
* class AttackAction implements INodeExecutor {
* execute(context: NodeExecutionContext): TaskStatus {
* console.log("执行攻击!");
* return TaskStatus.Success;
* }
* }
*
* // 2. 在行为树中使用
* BehaviorTreeBuilder.create("AI")
* .selector("Root")
* .action("AttackAction", "Attack")
* .end()
* .build();
* ```
*/
action(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addActionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加自定义条件节点
*
* @zh 直接使用注册的条件执行器类型
* @en Use a registered condition executor type directly
*/
condition(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addConditionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加黑板比较条件
*/

View File

@@ -5,7 +5,7 @@ export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
export { NodeMetadataRegistry } from './NodeMetadata';
export type { NodeMetadata, ConfigFieldDefinition } from './NodeMetadata';
export { NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
export * from './Executors';

View File

@@ -0,0 +1,57 @@
# @esengine/database-drivers
## 1.1.1
### Patch Changes
- [#412](https://github.com/esengine/esengine/pull/412) [`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20) Thanks [@esengine](https://github.com/esengine)! - fix: include dist directory in npm package
Previous 1.1.0 release was missing the compiled dist directory.
## 1.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - 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<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` 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 });
```

View File

@@ -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"
}

View File

@@ -0,0 +1,48 @@
{
"name": "@esengine/database-drivers",
"version": "1.1.1",
"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"
}
}

View File

@@ -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<T extends object> implements IMongoCollection<T> {
readonly name: string
constructor(private readonly _collection: Collection<T>) {
this.name = _collection.collectionName
}
// =========================================================================
// 查询 | Query
// =========================================================================
async findOne(filter: object, options?: FindOptions): Promise<T | null> {
const doc = await this._collection.findOne(
filter as Parameters<typeof this._collection.findOne>[0],
{
sort: options?.sort as Parameters<typeof this._collection.findOne>[1] extends { sort?: infer S } ? S : never,
projection: options?.projection
}
)
return doc ? this._stripId(doc) : null
}
async find(filter: object, options?: FindOptions): Promise<T[]> {
let cursor = this._collection.find(
filter as Parameters<typeof this._collection.find>[0]
)
if (options?.sort) {
cursor = cursor.sort(options.sort as Parameters<typeof cursor.sort>[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<number> {
return this._collection.countDocuments(
(filter ?? {}) as Parameters<typeof this._collection.countDocuments>[0]
)
}
// =========================================================================
// 创建 | Create
// =========================================================================
async insertOne(doc: T): Promise<InsertOneResult> {
const result = await this._collection.insertOne(
doc as Parameters<typeof this._collection.insertOne>[0]
)
return {
insertedId: result.insertedId,
acknowledged: result.acknowledged
}
}
async insertMany(docs: T[]): Promise<InsertManyResult> {
const result = await this._collection.insertMany(
docs as Parameters<typeof this._collection.insertMany>[0]
)
return {
insertedCount: result.insertedCount,
insertedIds: result.insertedIds as Record<number, unknown>,
acknowledged: result.acknowledged
}
}
// =========================================================================
// 更新 | Update
// =========================================================================
async updateOne(filter: object, update: object): Promise<UpdateResult> {
const result = await this._collection.updateOne(
filter as Parameters<typeof this._collection.updateOne>[0],
update as Parameters<typeof this._collection.updateOne>[1]
)
return {
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount,
upsertedCount: result.upsertedCount,
upsertedId: result.upsertedId,
acknowledged: result.acknowledged
}
}
async updateMany(filter: object, update: object): Promise<UpdateResult> {
const result = await this._collection.updateMany(
filter as Parameters<typeof this._collection.updateMany>[0],
update as Parameters<typeof this._collection.updateMany>[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<T | null> {
const result = await this._collection.findOneAndUpdate(
filter as Parameters<typeof this._collection.findOneAndUpdate>[0],
update as Parameters<typeof this._collection.findOneAndUpdate>[1],
{
returnDocument: options?.returnDocument ?? 'after',
upsert: options?.upsert
}
)
return result ? this._stripId(result) : null
}
// =========================================================================
// 删除 | Delete
// =========================================================================
async deleteOne(filter: object): Promise<DeleteResult> {
const result = await this._collection.deleteOne(
filter as Parameters<typeof this._collection.deleteOne>[0]
)
return {
deletedCount: result.deletedCount,
acknowledged: result.acknowledged
}
}
async deleteMany(filter: object): Promise<DeleteResult> {
const result = await this._collection.deleteMany(
filter as Parameters<typeof this._collection.deleteMany>[0]
)
return {
deletedCount: result.deletedCount,
acknowledged: result.acknowledged
}
}
// =========================================================================
// 索引 | Index
// =========================================================================
async createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string> {
return this._collection.createIndex(spec, options)
}
// =========================================================================
// 内部方法 | Internal Methods
// =========================================================================
/**
* @zh 移除 MongoDB 的 _id 字段
* @en Remove MongoDB's _id field
*/
private _stripId<D extends object>(doc: D): D {
const { _id, ...rest } = doc as { _id?: unknown } & Record<string, unknown>
return rest as D
}
}
/**
* @zh MongoDB 数据库适配器
* @en MongoDB database adapter
*/
export class MongoDatabaseAdapter implements IMongoDatabase {
readonly name: string
private _collections = new Map<string, MongoCollectionAdapter<object>>()
constructor(private readonly _db: Db) {
this.name = _db.databaseName
}
collection<T extends object = object>(name: string): IMongoCollection<T> {
if (!this._collections.has(name)) {
const nativeCollection = this._db.collection<T>(name)
this._collections.set(
name,
new MongoCollectionAdapter(nativeCollection) as MongoCollectionAdapter<object>
)
}
return this._collections.get(name) as IMongoCollection<T>
}
async listCollections(): Promise<string[]> {
const collections = await this._db.listCollections().toArray()
return collections.map(c => c.name)
}
async dropCollection(name: string): Promise<boolean> {
try {
await this._db.dropCollection(name)
this._collections.delete(name)
return true
} catch {
return false
}
}
}

View File

@@ -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<T extends object = object>(name: string): IMongoCollection<T>
}
/**
* @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<ConnectionEventType, Set<ConnectionEventListener>>()
private _reconnectAttempts = 0
private _reconnectTimer: ReturnType<typeof setTimeout> | 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<void> {
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<void> {
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<boolean> {
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<T extends object = object>(name: string): IMongoCollection<T> {
return this.getDatabase().collection<T>(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)
}

View File

@@ -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<string | null>
/**
* @zh 设置键值
* @en Set key value
*/
set(key: string, value: string, ttl?: number): Promise<void>
/**
* @zh 删除键
* @en Delete key
*/
del(key: string): Promise<boolean>
/**
* @zh 检查键是否存在
* @en Check if key exists
*/
exists(key: string): Promise<boolean>
}
/**
* @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<ConnectionEventType, Set<ConnectionEventListener>>()
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<void> {
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<void> {
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<boolean> {
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<string | null> {
return this.getClient().get(key)
}
async set(key: string, value: string, ttl?: number): Promise<void> {
const client = this.getClient()
if (ttl) {
await client.setex(key, ttl, value)
} else {
await client.set(key, value)
}
}
async del(key: string): Promise<boolean> {
const result = await this.getClient().del(key)
return result > 0
}
async exists(key: string): Promise<boolean> {
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)
}

View File

@@ -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'

View File

@@ -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'

View File

@@ -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<number, unknown>
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<string, SortDirection>
/**
* @zh 查找选项
* @en Find options
*/
export interface FindOptions {
sort?: Sort
limit?: number
skip?: number
projection?: Record<string, 0 | 1>
}
/**
* @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<T extends object> {
/**
* @zh 集合名称
* @en Collection name
*/
readonly name: string
// =========================================================================
// 查询 | Query
// =========================================================================
/**
* @zh 查找单条记录
* @en Find one document
*/
findOne(filter: object, options?: FindOptions): Promise<T | null>
/**
* @zh 查找多条记录
* @en Find documents
*/
find(filter: object, options?: FindOptions): Promise<T[]>
/**
* @zh 统计记录数
* @en Count documents
*/
countDocuments(filter?: object): Promise<number>
// =========================================================================
// 创建 | Create
// =========================================================================
/**
* @zh 插入单条记录
* @en Insert one document
*/
insertOne(doc: T): Promise<InsertOneResult>
/**
* @zh 批量插入
* @en Insert many documents
*/
insertMany(docs: T[]): Promise<InsertManyResult>
// =========================================================================
// 更新 | Update
// =========================================================================
/**
* @zh 更新单条记录
* @en Update one document
*/
updateOne(filter: object, update: object): Promise<UpdateResult>
/**
* @zh 批量更新
* @en Update many documents
*/
updateMany(filter: object, update: object): Promise<UpdateResult>
/**
* @zh 查找并更新
* @en Find one and update
*/
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// =========================================================================
// 删除 | Delete
// =========================================================================
/**
* @zh 删除单条记录
* @en Delete one document
*/
deleteOne(filter: object): Promise<DeleteResult>
/**
* @zh 批量删除
* @en Delete many documents
*/
deleteMany(filter: object): Promise<DeleteResult>
// =========================================================================
// 索引 | Index
// =========================================================================
/**
* @zh 创建索引
* @en Create index
*/
createIndex(spec: Record<string, 1 | -1>, options?: IndexOptions): Promise<string>
}
/**
* @zh MongoDB 数据库接口
* @en MongoDB database interface
*/
export interface IMongoDatabase {
/**
* @zh 数据库名称
* @en Database name
*/
readonly name: string
/**
* @zh 获取集合
* @en Get collection
*/
collection<T extends object = object>(name: string): IMongoCollection<T>
/**
* @zh 列出所有集合
* @en List all collections
*/
listCollections(): Promise<string[]>
/**
* @zh 删除集合
* @en Drop collection
*/
dropCollection(name: string): Promise<boolean>
}

View File

@@ -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<T> {
readonly id: string
readonly _type?: T
}
/**
* @zh 创建服务令牌
* @en Create service token
*/
export function createServiceToken<T>(id: string): ServiceToken<T> {
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<IMongoConnection>('database:mongo')
/**
* @zh Redis 连接令牌
* @en Redis connection token
*/
export const RedisConnectionToken = createServiceToken<IRedisConnection>('database:redis')

View File

@@ -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<void>
/**
* @zh 断开连接
* @en Disconnect
*/
disconnect(): Promise<void>
/**
* @zh 检查是否已连接
* @en Check if connected
*/
isConnected(): boolean
/**
* @zh 健康检查
* @en Health check
*/
ping(): Promise<boolean>
}
// =============================================================================
// 连接事件 | 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'
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declarationDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -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,
});

View File

@@ -0,0 +1,65 @@
# @esengine/database
## 1.1.1
### Patch Changes
- [#412](https://github.com/esengine/esengine/pull/412) [`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20) Thanks [@esengine](https://github.com/esengine)! - fix: include dist directory in npm package
Previous 1.1.0 release was missing the compiled dist directory.
- Updated dependencies [[`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20)]:
- @esengine/database-drivers@1.1.1
## 1.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - 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<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` 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 });
```
### Patch Changes
- Updated dependencies [[`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa)]:
- @esengine/database-drivers@1.1.0

View File

@@ -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"
}

View File

@@ -0,0 +1,37 @@
{
"name": "@esengine/database",
"version": "1.1.1",
"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"
}
}

View File

@@ -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<Player> {
* constructor(connection: IMongoConnection) {
* super(connection, 'players')
* }
*
* async findTopPlayers(limit: number): Promise<Player[]> {
* return this.findMany({
* sort: { score: 'desc' },
* limit,
* })
* }
* }
* ```
*/
export class Repository<T extends BaseEntity> implements IRepository<T> {
protected readonly _collection: IMongoCollection<T>
constructor(
protected readonly connection: IMongoConnection,
public readonly collectionName: string,
protected readonly enableSoftDelete: boolean = false
) {
this._collection = connection.collection<T>(collectionName)
}
// =========================================================================
// 查询 | Query
// =========================================================================
async findById(id: string): Promise<T | null> {
const filter = this._buildFilter({ where: { id } as WhereCondition<T> })
return this._collection.findOne(filter)
}
async findOne(options?: QueryOptions<T>): Promise<T | null> {
const filter = this._buildFilter(options)
const sort = this._buildSort(options)
return this._collection.findOne(filter, { sort })
}
async findMany(options?: QueryOptions<T>): Promise<T[]> {
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<QueryOptions<T>, 'limit' | 'offset'>
): Promise<PaginatedResult<T>> {
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<T>): Promise<number> {
const filter = this._buildFilter(options)
return this._collection.countDocuments(filter)
}
async exists(options: QueryOptions<T>): Promise<boolean> {
const count = await this.count({ ...options, limit: 1 })
return count > 0
}
// =========================================================================
// 创建 | Create
// =========================================================================
async create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }): Promise<T> {
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<Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }>
): Promise<T[]> {
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<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>
): Promise<T | null> {
const filter = this._buildFilter({ where: { id } as WhereCondition<T> })
return this._collection.findOneAndUpdate(
filter,
{ $set: { ...data, updatedAt: new Date() } },
{ returnDocument: 'after' }
)
}
// =========================================================================
// 删除 | Delete
// =========================================================================
async delete(id: string): Promise<boolean> {
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<T>): Promise<number> {
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<T | null> {
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<T>): object {
const filter: Record<string, unknown> = {}
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<T>): object {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(where)) {
if (key === '$or' && Array.isArray(value)) {
result['$or'] = value.map(v => this._convertWhere(v as WhereCondition<T>))
continue
}
if (key === '$and' && Array.isArray(value)) {
result['$and'] = value.map(v => this._convertWhere(v as WhereCondition<T>))
continue
}
if (value === undefined) continue
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
const ops = value as Record<string, unknown>
const mongoOps: Record<string, unknown> = {}
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<T>): Record<string, 1 | -1> | undefined {
if (!options?.sort) return undefined
const result: Record<string, 1 | -1> = {}
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<T extends BaseEntity>(
connection: IMongoConnection,
collectionName: string,
enableSoftDelete = false
): Repository<T> {
return new Repository<T>(connection, collectionName, enableSoftDelete)
}

View File

@@ -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<string, unknown>
}
/**
* @zh 用户信息(不含密码)
* @en User info (without password)
*/
export type SafeUser = Omit<UserEntity, 'passwordHash'>
/**
* @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<UserEntity> {
constructor(connection: IMongoConnection, collectionName = 'users') {
super(connection, collectionName, true)
}
// =========================================================================
// 查询 | Query
// =========================================================================
/**
* @zh 根据用户名查找用户
* @en Find user by username
*/
async findByUsername(username: string): Promise<UserEntity | null> {
return this.findOne({ where: { username } })
}
/**
* @zh 根据邮箱查找用户
* @en Find user by email
*/
async findByEmail(email: string): Promise<UserEntity | null> {
return this.findOne({ where: { email } })
}
/**
* @zh 检查用户名是否存在
* @en Check if username exists
*/
async usernameExists(username: string): Promise<boolean> {
return this.exists({ where: { username } })
}
/**
* @zh 检查邮箱是否存在
* @en Check if email exists
*/
async emailExists(email: string): Promise<boolean> {
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<SafeUser> {
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<SafeUser | null> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
const result = await this.update(userId, { isActive: false })
return result !== null
}
/**
* @zh 启用用户
* @en Activate user
*/
async activate(userId: string): Promise<boolean> {
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)
}

View File

@@ -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<Player> {
* constructor(connection: IMongoConnection) {
* super(connection, 'players')
* }
*
* async findTopPlayers(limit = 10): Promise<Player[]> {
* return this.findMany({
* sort: { score: 'desc' },
* limit,
* })
* }
*
* async addScore(playerId: string, points: number): Promise<Player | null> {
* 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'

View File

@@ -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<PasswordHashConfig> = {
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<string> {
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<boolean> {
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 }
}

View File

@@ -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<UserRepository> = { id: 'database:userRepository' }

View File

@@ -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<T> {
$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<T> = {
[K in keyof T]?: T[K] | ComparisonOperators<T[K]>
} & {
$or?: WhereCondition<T>[]
$and?: WhereCondition<T>[]
}
/**
* @zh 排序方向
* @en Sort direction
*/
export type SortDirection = 'asc' | 'desc'
/**
* @zh 排序条件
* @en Sort condition
*/
export type SortCondition<T> = {
[K in keyof T]?: SortDirection
}
/**
* @zh 查询选项
* @en Query options
*/
export interface QueryOptions<T> {
/**
* @zh 过滤条件
* @en Filter conditions
*/
where?: WhereCondition<T>
/**
* @zh 排序条件
* @en Sort conditions
*/
sort?: SortCondition<T>
/**
* @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<T> {
/**
* @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<T extends BaseEntity> {
/**
* @zh 集合名称
* @en Collection name
*/
readonly collectionName: string
/**
* @zh 根据 ID 查找
* @en Find by ID
*/
findById(id: string): Promise<T | null>
/**
* @zh 查找单条记录
* @en Find one record
*/
findOne(options?: QueryOptions<T>): Promise<T | null>
/**
* @zh 查找多条记录
* @en Find many records
*/
findMany(options?: QueryOptions<T>): Promise<T[]>
/**
* @zh 分页查询
* @en Paginated query
*/
findPaginated(
pagination: PaginationParams,
options?: Omit<QueryOptions<T>, 'limit' | 'offset'>
): Promise<PaginatedResult<T>>
/**
* @zh 统计记录数
* @en Count records
*/
count(options?: QueryOptions<T>): Promise<number>
/**
* @zh 检查记录是否存在
* @en Check if record exists
*/
exists(options: QueryOptions<T>): Promise<boolean>
/**
* @zh 创建记录
* @en Create record
*/
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }): Promise<T>
/**
* @zh 批量创建
* @en Bulk create
*/
createMany(data: Array<Omit<T, 'id' | 'createdAt' | 'updatedAt'> & { id?: string }>): Promise<T[]>
/**
* @zh 更新记录
* @en Update record
*/
update(id: string, data: Partial<Omit<T, 'id' | 'createdAt' | 'updatedAt'>>): Promise<T | null>
/**
* @zh 删除记录
* @en Delete record
*/
delete(id: string): Promise<boolean>
/**
* @zh 批量删除
* @en Bulk delete
*/
deleteMany(options: QueryOptions<T>): Promise<number>
}
// =============================================================================
// 用户实体 | 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<string, unknown>
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declarationDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -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,
});

View File

@@ -1,5 +1,65 @@
# @esengine/transaction
## 2.1.1
### Patch Changes
- Updated dependencies [[`85171a0`](https://github.com/esengine/esengine/commit/85171a0a5c073ef7883705ee4daaca8bb0218f20)]:
- @esengine/database-drivers@1.1.1
## 2.1.0
### Minor Changes
- [#410](https://github.com/esengine/esengine/pull/410) [`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa) Thanks [@esengine](https://github.com/esengine)! - 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<T>` interface decoupled from mongodb types
- Service tokens for dependency injection (`MongoConnectionToken`, `RedisConnectionToken`)
**@esengine/database (Layer 2)**
- Generic `Repository<T>` 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 });
```
### Patch Changes
- Updated dependencies [[`71022ab`](https://github.com/esengine/esengine/commit/71022abc99ad4a1b349f19f4ccf1e0a2a0923dfa)]:
- @esengine/database-drivers@1.1.0
## 2.0.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/transaction",
"version": "2.0.7",
"version": "2.1.1",
"description": "Game transaction system with distributed support | 游戏事务系统,支持分布式事务",
"type": "module",
"main": "./dist/index.js",
@@ -25,7 +25,7 @@
"test:watch": "vitest"
},
"dependencies": {
"@esengine/server": "workspace:*"
"@esengine/database-drivers": "workspace:*"
},
"peerDependencies": {
"ioredis": "^5.3.0",

View File

@@ -88,9 +88,7 @@ export {
export {
MongoStorage,
createMongoStorage,
type MongoStorageConfig,
type MongoDb,
type MongoCollection
type MongoStorageConfig
} from './storage/MongoStorage.js';
// =============================================================================

View File

@@ -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<T> {
findOne(filter: object): Promise<T | null>
find(filter: object): {
toArray(): Promise<T[]>
}
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<string>
}
/**
* @zh MongoDB 数据库接口
* @en MongoDB database interface
*/
export interface MongoDb {
collection<T = unknown>(name: string): MongoCollection<T>
}
/**
* @zh MongoDB 客户端接口
* @en MongoDB client interface
*/
export interface MongoClient {
db(name?: string): MongoDb
close(): Promise<void>
}
/**
* @zh MongoDB 连接工厂
* @en MongoDB connection factory
*/
export type MongoClientFactory = () => MongoClient | Promise<MongoClient>
// =============================================================================
// 配置类型 | Configuration Types
// =============================================================================
/**
* @zh MongoDB 存储配置
@@ -57,29 +24,10 @@ export type MongoClientFactory = () => MongoClient | Promise<MongoClient>
*/
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<MongoDb> {
private _getCollection<T extends object>(name: string): IMongoCollection<T> {
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<T>(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<void> {
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<void> {
const db = await this._getDb();
const txColl = db.collection<TransactionLog>(this._transactionCollection);
const txColl = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
await txColl.createIndex({ state: 1 });
await txColl.createIndex({ 'metadata.serverId': 1 });
await txColl.createIndex({ createdAt: 1 });
const lockColl = db.collection<LockDocument>(this._lockCollection);
const lockColl = this._getCollection<LockDocument>(this._lockCollection);
await lockColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
const dataColl = db.collection<DataDocument>(this._dataCollection);
const dataColl = this._getCollection<DataDocument>(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<string | null> {
const db = await this._getDb();
const coll = db.collection<LockDocument>(this._lockCollection);
const coll = this._getCollection<LockDocument>(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<boolean> {
const db = await this._getDb();
const coll = db.collection<LockDocument>(this._lockCollection);
const coll = this._getCollection<LockDocument>(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<void> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(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<TransactionLog | null> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(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<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(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<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const update: Record<string, unknown> = {
[`operations.${operationIndex}.state`]: state,
@@ -333,8 +258,7 @@ export class MongoStorage implements ITransactionStorage {
}
async getPendingTransactions(serverId?: string): Promise<TransactionLog[]> {
const db = await this._getDb();
const coll = db.collection<TransactionLog & { _id: string }>(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
const filter: Record<string, unknown> = {
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<void> {
const db = await this._getDb();
const coll = db.collection(this._transactionCollection);
const coll = this._getCollection<TransactionLog & { _id: string }>(this._transactionCollection);
await coll.deleteOne({ _id: id });
}
@@ -359,8 +282,7 @@ export class MongoStorage implements ITransactionStorage {
// =========================================================================
async get<T>(key: string): Promise<T | null> {
const db = await this._getDb();
const coll = db.collection<DataDocument>(this._dataCollection);
const coll = this._getCollection<DataDocument>(this._dataCollection);
const doc = await coll.findOne({ _id: key });
if (!doc) return null;
@@ -374,13 +296,9 @@ export class MongoStorage implements ITransactionStorage {
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const db = await this._getDb();
const coll = db.collection<DataDocument>(this._dataCollection);
const coll = this._getCollection<DataDocument>(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<boolean> {
const db = await this._getDb();
const coll = db.collection(this._dataCollection);
const coll = this._getCollection<DataDocument>(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<MongoStorageConfig, 'connection'>
): MongoStorage {
return new MongoStorage({ connection, ...options });
}

View File

@@ -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';

View File

@@ -6,7 +6,7 @@ export default defineConfig({
dts: true,
sourcemap: true,
clean: true,
external: ["../pkg/rapier_wasm2d.js"],
external: [/\.\.\/pkg\/rapier_wasm2d/],
loader: {
".wasm": "base64",
},

44
pnpm-lock.yaml generated
View File

@@ -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