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
This commit is contained in:
YHH
2025-12-31 16:26:53 +08:00
committed by GitHub
parent 87f71e2251
commit 71022abc99
41 changed files with 5226 additions and 186 deletions

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.0.0",
"description": "Database connection drivers for ESEngine | ESEngine 数据库连接驱动",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"peerDependencies": {
"mongodb": ">=6.0.0",
"ioredis": ">=5.0.0"
},
"peerDependenciesMeta": {
"mongodb": {
"optional": true
},
"ioredis": {
"optional": true
}
},
"devDependencies": {
"@types/node": "^20.0.0",
"mongodb": "^6.12.0",
"ioredis": "^5.3.0",
"tsup": "^8.0.0",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

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,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.0.0",
"description": "Database CRUD operations and repositories for ESEngine | ESEngine 数据库 CRUD 操作和仓库",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/database-drivers": "workspace:*"
},
"devDependencies": {
"@types/node": "^20.0.0",
"tsup": "^8.0.0",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

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

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