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:
313
packages/framework/database/src/Repository.ts
Normal file
313
packages/framework/database/src/Repository.ts
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user