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