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,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'
}
}