docs: add database and database-drivers to sidebar navigation (#414)
- Add database module navigation (repository, user, query) - Add database-drivers module navigation (mongo, redis) - Create missing English documentation files for database module - Create missing English documentation files for database-drivers module
This commit is contained in:
@@ -287,6 +287,25 @@ export default defineConfig({
|
|||||||
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
|
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '数据库',
|
||||||
|
translations: { en: 'Database' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/database', translations: { en: 'Overview' } },
|
||||||
|
{ label: '仓储模式', slug: 'modules/database/repository', translations: { en: 'Repository' } },
|
||||||
|
{ label: '用户仓储', slug: 'modules/database/user', translations: { en: 'User Repository' } },
|
||||||
|
{ label: '查询构建器', slug: 'modules/database/query', translations: { en: 'Query Builder' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '数据库驱动',
|
||||||
|
translations: { en: 'Database Drivers' },
|
||||||
|
items: [
|
||||||
|
{ label: '概述', slug: 'modules/database-drivers', translations: { en: 'Overview' } },
|
||||||
|
{ label: 'MongoDB', slug: 'modules/database-drivers/mongo', translations: { en: 'MongoDB' } },
|
||||||
|
{ label: 'Redis', slug: 'modules/database-drivers/redis', translations: { en: 'Redis' } },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '世界流式加载',
|
label: '世界流式加载',
|
||||||
translations: { en: 'World Streaming' },
|
translations: { en: 'World Streaming' },
|
||||||
|
|||||||
265
docs/src/content/docs/en/modules/database-drivers/mongo.md
Normal file
265
docs/src/content/docs/en/modules/database-drivers/mongo.md
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
---
|
||||||
|
title: "MongoDB Connection"
|
||||||
|
description: "MongoDB connection management, connection pooling, auto-reconnect"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MongoConnectionConfig {
|
||||||
|
/** MongoDB connection URI */
|
||||||
|
uri: string
|
||||||
|
|
||||||
|
/** Database name */
|
||||||
|
database: string
|
||||||
|
|
||||||
|
/** Connection pool configuration */
|
||||||
|
pool?: {
|
||||||
|
minSize?: number // Minimum connections
|
||||||
|
maxSize?: number // Maximum connections
|
||||||
|
acquireTimeout?: number // Connection acquire timeout (ms)
|
||||||
|
maxLifetime?: number // Maximum connection lifetime (ms)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Auto-reconnect (default true) */
|
||||||
|
autoReconnect?: boolean
|
||||||
|
|
||||||
|
/** Reconnect interval (ms, default 5000) */
|
||||||
|
reconnectInterval?: number
|
||||||
|
|
||||||
|
/** Maximum reconnect attempts (default 10) */
|
||||||
|
maxReconnectAttempts?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMongoConnection, MongoConnectionToken } from '@esengine/database-drivers'
|
||||||
|
|
||||||
|
const mongo = createMongoConnection({
|
||||||
|
uri: 'mongodb://localhost:27017',
|
||||||
|
database: 'game',
|
||||||
|
pool: {
|
||||||
|
minSize: 5,
|
||||||
|
maxSize: 20,
|
||||||
|
acquireTimeout: 5000,
|
||||||
|
maxLifetime: 300000
|
||||||
|
},
|
||||||
|
autoReconnect: true,
|
||||||
|
reconnectInterval: 5000,
|
||||||
|
maxReconnectAttempts: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
mongo.on('connected', () => {
|
||||||
|
console.log('MongoDB connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
mongo.on('disconnected', () => {
|
||||||
|
console.log('MongoDB disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
mongo.on('reconnecting', () => {
|
||||||
|
console.log('MongoDB reconnecting...')
|
||||||
|
})
|
||||||
|
|
||||||
|
mongo.on('reconnected', () => {
|
||||||
|
console.log('MongoDB reconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
mongo.on('error', (event) => {
|
||||||
|
console.error('MongoDB error:', event.error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
await mongo.connect()
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
console.log('Connected:', mongo.isConnected())
|
||||||
|
console.log('Ping:', await mongo.ping())
|
||||||
|
```
|
||||||
|
|
||||||
|
## IMongoConnection Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IMongoConnection {
|
||||||
|
/** Connection ID */
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
/** Connection state */
|
||||||
|
readonly state: ConnectionState
|
||||||
|
|
||||||
|
/** Establish connection */
|
||||||
|
connect(): Promise<void>
|
||||||
|
|
||||||
|
/** Disconnect */
|
||||||
|
disconnect(): Promise<void>
|
||||||
|
|
||||||
|
/** Check if connected */
|
||||||
|
isConnected(): boolean
|
||||||
|
|
||||||
|
/** Test connection */
|
||||||
|
ping(): Promise<boolean>
|
||||||
|
|
||||||
|
/** Get typed collection */
|
||||||
|
collection<T extends object>(name: string): IMongoCollection<T>
|
||||||
|
|
||||||
|
/** Get database interface */
|
||||||
|
getDatabase(): IMongoDatabase
|
||||||
|
|
||||||
|
/** Get native client (advanced usage) */
|
||||||
|
getNativeClient(): MongoClientType
|
||||||
|
|
||||||
|
/** Get native database (advanced usage) */
|
||||||
|
getNativeDatabase(): Db
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## IMongoCollection Interface
|
||||||
|
|
||||||
|
Type-safe collection interface, decoupled from native MongoDB types:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IMongoCollection<T extends object> {
|
||||||
|
readonly name: string
|
||||||
|
|
||||||
|
// Query
|
||||||
|
findOne(filter: object, options?: FindOptions): Promise<T | null>
|
||||||
|
find(filter: object, options?: FindOptions): Promise<T[]>
|
||||||
|
countDocuments(filter?: object): Promise<number>
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
insertOne(doc: T): Promise<InsertOneResult>
|
||||||
|
insertMany(docs: T[]): Promise<InsertManyResult>
|
||||||
|
|
||||||
|
// Update
|
||||||
|
updateOne(filter: object, update: object): Promise<UpdateResult>
|
||||||
|
updateMany(filter: object, update: object): Promise<UpdateResult>
|
||||||
|
findOneAndUpdate(
|
||||||
|
filter: object,
|
||||||
|
update: object,
|
||||||
|
options?: FindOneAndUpdateOptions
|
||||||
|
): Promise<T | null>
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
deleteOne(filter: object): Promise<DeleteResult>
|
||||||
|
deleteMany(filter: object): Promise<DeleteResult>
|
||||||
|
|
||||||
|
// Index
|
||||||
|
createIndex(
|
||||||
|
spec: Record<string, 1 | -1>,
|
||||||
|
options?: IndexOptions
|
||||||
|
): Promise<string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic CRUD
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface User {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = mongo.collection<User>('users')
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
await users.insertOne({
|
||||||
|
id: '1',
|
||||||
|
name: 'John',
|
||||||
|
email: 'john@example.com',
|
||||||
|
score: 100
|
||||||
|
})
|
||||||
|
|
||||||
|
// Query
|
||||||
|
const user = await users.findOne({ name: 'John' })
|
||||||
|
|
||||||
|
const topUsers = await users.find(
|
||||||
|
{ score: { $gte: 100 } },
|
||||||
|
{ sort: { score: -1 }, limit: 10 }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update
|
||||||
|
await users.updateOne(
|
||||||
|
{ id: '1' },
|
||||||
|
{ $inc: { score: 10 } }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
await users.deleteOne({ id: '1' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Batch insert
|
||||||
|
await users.insertMany([
|
||||||
|
{ id: '1', name: 'Alice', email: 'alice@example.com', score: 100 },
|
||||||
|
{ id: '2', name: 'Bob', email: 'bob@example.com', score: 200 },
|
||||||
|
{ id: '3', name: 'Carol', email: 'carol@example.com', score: 150 }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Batch update
|
||||||
|
await users.updateMany(
|
||||||
|
{ score: { $lt: 100 } },
|
||||||
|
{ $set: { status: 'inactive' } }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Batch delete
|
||||||
|
await users.deleteMany({ status: 'inactive' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Index Management
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Create indexes
|
||||||
|
await users.createIndex({ email: 1 }, { unique: true })
|
||||||
|
await users.createIndex({ score: -1 })
|
||||||
|
await users.createIndex({ name: 1, score: -1 })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Other Modules
|
||||||
|
|
||||||
|
### With @esengine/database
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMongoConnection } from '@esengine/database-drivers'
|
||||||
|
import { UserRepository, createRepository } from '@esengine/database'
|
||||||
|
|
||||||
|
const mongo = createMongoConnection({
|
||||||
|
uri: 'mongodb://localhost:27017',
|
||||||
|
database: 'game'
|
||||||
|
})
|
||||||
|
await mongo.connect()
|
||||||
|
|
||||||
|
// Use UserRepository
|
||||||
|
const userRepo = new UserRepository(mongo)
|
||||||
|
await userRepo.register({ username: 'john', password: '123456' })
|
||||||
|
|
||||||
|
// Use generic repository
|
||||||
|
const playerRepo = createRepository<Player>(mongo, 'players')
|
||||||
|
```
|
||||||
|
|
||||||
|
### With @esengine/transaction
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMongoConnection } from '@esengine/database-drivers'
|
||||||
|
import { createMongoStorage, TransactionManager } from '@esengine/transaction'
|
||||||
|
|
||||||
|
const mongo = createMongoConnection({
|
||||||
|
uri: 'mongodb://localhost:27017',
|
||||||
|
database: 'game'
|
||||||
|
})
|
||||||
|
await mongo.connect()
|
||||||
|
|
||||||
|
// Create transaction storage (shared connection)
|
||||||
|
const storage = createMongoStorage(mongo)
|
||||||
|
await storage.ensureIndexes()
|
||||||
|
|
||||||
|
const txManager = new TransactionManager({ storage })
|
||||||
|
```
|
||||||
228
docs/src/content/docs/en/modules/database-drivers/redis.md
Normal file
228
docs/src/content/docs/en/modules/database-drivers/redis.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
---
|
||||||
|
title: "Redis Connection"
|
||||||
|
description: "Redis connection management, auto-reconnect, key prefix"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface RedisConnectionConfig {
|
||||||
|
/** Redis host */
|
||||||
|
host?: string
|
||||||
|
|
||||||
|
/** Redis port */
|
||||||
|
port?: number
|
||||||
|
|
||||||
|
/** Authentication password */
|
||||||
|
password?: string
|
||||||
|
|
||||||
|
/** Database number */
|
||||||
|
db?: number
|
||||||
|
|
||||||
|
/** Key prefix */
|
||||||
|
keyPrefix?: string
|
||||||
|
|
||||||
|
/** Auto-reconnect (default true) */
|
||||||
|
autoReconnect?: boolean
|
||||||
|
|
||||||
|
/** Reconnect interval (ms, default 5000) */
|
||||||
|
reconnectInterval?: number
|
||||||
|
|
||||||
|
/** Maximum reconnect attempts (default 10) */
|
||||||
|
maxReconnectAttempts?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Example
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRedisConnection, RedisConnectionToken } from '@esengine/database-drivers'
|
||||||
|
|
||||||
|
const redis = createRedisConnection({
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
password: 'your-password',
|
||||||
|
db: 0,
|
||||||
|
keyPrefix: 'game:',
|
||||||
|
autoReconnect: true,
|
||||||
|
reconnectInterval: 5000,
|
||||||
|
maxReconnectAttempts: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
redis.on('connected', () => {
|
||||||
|
console.log('Redis connected')
|
||||||
|
})
|
||||||
|
|
||||||
|
redis.on('disconnected', () => {
|
||||||
|
console.log('Redis disconnected')
|
||||||
|
})
|
||||||
|
|
||||||
|
redis.on('error', (event) => {
|
||||||
|
console.error('Redis error:', event.error)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
await redis.connect()
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
console.log('Connected:', redis.isConnected())
|
||||||
|
console.log('Ping:', await redis.ping())
|
||||||
|
```
|
||||||
|
|
||||||
|
## IRedisConnection Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface IRedisConnection {
|
||||||
|
/** Connection ID */
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
/** Connection state */
|
||||||
|
readonly state: ConnectionState
|
||||||
|
|
||||||
|
/** Establish connection */
|
||||||
|
connect(): Promise<void>
|
||||||
|
|
||||||
|
/** Disconnect */
|
||||||
|
disconnect(): Promise<void>
|
||||||
|
|
||||||
|
/** Check if connected */
|
||||||
|
isConnected(): boolean
|
||||||
|
|
||||||
|
/** Test connection */
|
||||||
|
ping(): Promise<boolean>
|
||||||
|
|
||||||
|
/** Get value */
|
||||||
|
get(key: string): Promise<string | null>
|
||||||
|
|
||||||
|
/** Set value (optional TTL in seconds) */
|
||||||
|
set(key: string, value: string, ttl?: number): Promise<void>
|
||||||
|
|
||||||
|
/** Delete key */
|
||||||
|
del(key: string): Promise<boolean>
|
||||||
|
|
||||||
|
/** Check if key exists */
|
||||||
|
exists(key: string): Promise<boolean>
|
||||||
|
|
||||||
|
/** Set expiration (seconds) */
|
||||||
|
expire(key: string, seconds: number): Promise<boolean>
|
||||||
|
|
||||||
|
/** Get remaining TTL (seconds) */
|
||||||
|
ttl(key: string): Promise<number>
|
||||||
|
|
||||||
|
/** Get native client (advanced usage) */
|
||||||
|
getNativeClient(): Redis
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### Basic Operations
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Set value
|
||||||
|
await redis.set('user:1:name', 'John')
|
||||||
|
|
||||||
|
// Set value with expiration (1 hour)
|
||||||
|
await redis.set('session:abc123', 'user-data', 3600)
|
||||||
|
|
||||||
|
// Get value
|
||||||
|
const name = await redis.get('user:1:name')
|
||||||
|
|
||||||
|
// Check if key exists
|
||||||
|
const exists = await redis.exists('user:1:name')
|
||||||
|
|
||||||
|
// Delete key
|
||||||
|
await redis.del('user:1:name')
|
||||||
|
|
||||||
|
// Get remaining TTL
|
||||||
|
const ttl = await redis.ttl('session:abc123')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Prefix
|
||||||
|
|
||||||
|
When `keyPrefix` is configured, all operations automatically add the prefix:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const redis = createRedisConnection({
|
||||||
|
host: 'localhost',
|
||||||
|
keyPrefix: 'game:'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Actual key is 'game:user:1'
|
||||||
|
await redis.set('user:1', 'data')
|
||||||
|
|
||||||
|
// Actual key queried is 'game:user:1'
|
||||||
|
const data = await redis.get('user:1')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Operations
|
||||||
|
|
||||||
|
Use native client for advanced operations:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const client = redis.getNativeClient()
|
||||||
|
|
||||||
|
// Using Pipeline
|
||||||
|
const pipeline = client.pipeline()
|
||||||
|
pipeline.set('key1', 'value1')
|
||||||
|
pipeline.set('key2', 'value2')
|
||||||
|
pipeline.set('key3', 'value3')
|
||||||
|
await pipeline.exec()
|
||||||
|
|
||||||
|
// Using Transactions
|
||||||
|
const multi = client.multi()
|
||||||
|
multi.incr('counter')
|
||||||
|
multi.get('counter')
|
||||||
|
const results = await multi.exec()
|
||||||
|
|
||||||
|
// Using Lua Scripts
|
||||||
|
const result = await client.eval(
|
||||||
|
`return redis.call('get', KEYS[1])`,
|
||||||
|
1,
|
||||||
|
'mykey'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Transaction System
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRedisConnection } from '@esengine/database-drivers'
|
||||||
|
import { RedisStorage, TransactionManager } from '@esengine/transaction'
|
||||||
|
|
||||||
|
const redis = createRedisConnection({
|
||||||
|
host: 'localhost',
|
||||||
|
port: 6379,
|
||||||
|
keyPrefix: 'tx:'
|
||||||
|
})
|
||||||
|
await redis.connect()
|
||||||
|
|
||||||
|
// Create transaction storage
|
||||||
|
const storage = new RedisStorage({
|
||||||
|
factory: () => redis.getNativeClient(),
|
||||||
|
prefix: 'tx:'
|
||||||
|
})
|
||||||
|
|
||||||
|
const txManager = new TransactionManager({ storage })
|
||||||
|
```
|
||||||
|
|
||||||
|
## Connection State
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ConnectionState =
|
||||||
|
| 'disconnected' // Not connected
|
||||||
|
| 'connecting' // Connecting
|
||||||
|
| 'connected' // Connected
|
||||||
|
| 'disconnecting' // Disconnecting
|
||||||
|
| 'error' // Error state
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
| Event | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `connected` | Connection established |
|
||||||
|
| `disconnected` | Connection closed |
|
||||||
|
| `reconnecting` | Reconnecting |
|
||||||
|
| `reconnected` | Reconnection successful |
|
||||||
|
| `error` | Error occurred |
|
||||||
185
docs/src/content/docs/en/modules/database/query.md
Normal file
185
docs/src/content/docs/en/modules/database/query.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
---
|
||||||
|
title: "Query Syntax"
|
||||||
|
description: "Query condition operators and syntax"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Queries
|
||||||
|
|
||||||
|
### Exact Match
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
where: {
|
||||||
|
name: 'John',
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Operators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
where: {
|
||||||
|
score: { $gte: 100 },
|
||||||
|
rank: { $in: ['gold', 'platinum'] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Operators
|
||||||
|
|
||||||
|
| Operator | Description | Example |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `$eq` | Equal | `{ score: { $eq: 100 } }` |
|
||||||
|
| `$ne` | Not equal | `{ status: { $ne: 'banned' } }` |
|
||||||
|
| `$gt` | Greater than | `{ score: { $gt: 50 } }` |
|
||||||
|
| `$gte` | Greater than or equal | `{ level: { $gte: 10 } }` |
|
||||||
|
| `$lt` | Less than | `{ age: { $lt: 18 } }` |
|
||||||
|
| `$lte` | Less than or equal | `{ price: { $lte: 100 } }` |
|
||||||
|
| `$in` | In array | `{ rank: { $in: ['gold', 'platinum'] } }` |
|
||||||
|
| `$nin` | Not in array | `{ status: { $nin: ['banned', 'suspended'] } }` |
|
||||||
|
| `$like` | Pattern match | `{ name: { $like: '%john%' } }` |
|
||||||
|
| `$regex` | Regex match | `{ email: { $regex: '@gmail.com$' } }` |
|
||||||
|
|
||||||
|
## Logical Operators
|
||||||
|
|
||||||
|
### $or
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
where: {
|
||||||
|
$or: [
|
||||||
|
{ score: { $gte: 1000 } },
|
||||||
|
{ rank: 'legendary' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### $and
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
where: {
|
||||||
|
$and: [
|
||||||
|
{ score: { $gte: 100 } },
|
||||||
|
{ score: { $lte: 500 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'active',
|
||||||
|
$or: [
|
||||||
|
{ rank: 'gold' },
|
||||||
|
{ score: { $gte: 1000 } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pattern Matching
|
||||||
|
|
||||||
|
### $like Syntax
|
||||||
|
|
||||||
|
- `%` - Matches any sequence of characters
|
||||||
|
- `_` - Matches single character
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Starts with 'John'
|
||||||
|
{ name: { $like: 'John%' } }
|
||||||
|
|
||||||
|
// Ends with 'son'
|
||||||
|
{ name: { $like: '%son' } }
|
||||||
|
|
||||||
|
// Contains 'oh'
|
||||||
|
{ name: { $like: '%oh%' } }
|
||||||
|
|
||||||
|
// Second character is 'o'
|
||||||
|
{ name: { $like: '_o%' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### $regex Syntax
|
||||||
|
|
||||||
|
Uses standard regular expressions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Starts with 'John' (case insensitive)
|
||||||
|
{ name: { $regex: '^john' } }
|
||||||
|
|
||||||
|
// Gmail email
|
||||||
|
{ email: { $regex: '@gmail\\.com$' } }
|
||||||
|
|
||||||
|
// Contains numbers
|
||||||
|
{ username: { $regex: '\\d+' } }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sorting
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.findMany({
|
||||||
|
sort: {
|
||||||
|
score: 'desc', // Descending
|
||||||
|
name: 'asc' // Ascending
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
### Using limit/offset
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// First page
|
||||||
|
await repo.findMany({
|
||||||
|
limit: 20,
|
||||||
|
offset: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second page
|
||||||
|
await repo.findMany({
|
||||||
|
limit: 20,
|
||||||
|
offset: 20
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using findPaginated
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await repo.findPaginated(
|
||||||
|
{ page: 2, pageSize: 20 },
|
||||||
|
{ sort: { createdAt: 'desc' } }
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Examples
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find active gold players with scores between 100-1000
|
||||||
|
// Sort by score descending, get top 10
|
||||||
|
const players = await repo.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'active',
|
||||||
|
rank: 'gold',
|
||||||
|
score: { $gte: 100, $lte: 1000 }
|
||||||
|
},
|
||||||
|
sort: { score: 'desc' },
|
||||||
|
limit: 10
|
||||||
|
})
|
||||||
|
|
||||||
|
// Search for users with 'john' in username or gmail email
|
||||||
|
const users = await repo.findMany({
|
||||||
|
where: {
|
||||||
|
$or: [
|
||||||
|
{ username: { $like: '%john%' } },
|
||||||
|
{ email: { $regex: '@gmail\\.com$' } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
244
docs/src/content/docs/en/modules/database/repository.md
Normal file
244
docs/src/content/docs/en/modules/database/repository.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
---
|
||||||
|
title: "Repository API"
|
||||||
|
description: "Generic repository interface, CRUD operations, pagination, soft delete"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Creating a Repository
|
||||||
|
|
||||||
|
### Using Factory Function
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createRepository } from '@esengine/database'
|
||||||
|
|
||||||
|
const playerRepo = createRepository<Player>(mongo, 'players')
|
||||||
|
|
||||||
|
// Enable soft delete
|
||||||
|
const playerRepo = createRepository<Player>(mongo, 'players', true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extending Repository
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Repository, BaseEntity } from '@esengine/database'
|
||||||
|
|
||||||
|
interface Player extends BaseEntity {
|
||||||
|
name: string
|
||||||
|
score: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerRepository extends Repository<Player> {
|
||||||
|
constructor(connection: IMongoConnection) {
|
||||||
|
super(connection, 'players', false) // Third param: enable soft delete
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom methods
|
||||||
|
async findTopPlayers(limit: number): Promise<Player[]> {
|
||||||
|
return this.findMany({
|
||||||
|
sort: { score: 'desc' },
|
||||||
|
limit
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## BaseEntity Interface
|
||||||
|
|
||||||
|
All entities must extend `BaseEntity`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BaseEntity {
|
||||||
|
id: string
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
|
deletedAt?: Date // Used for soft delete
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Methods
|
||||||
|
|
||||||
|
### findById
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const player = await repo.findById('player-123')
|
||||||
|
```
|
||||||
|
|
||||||
|
### findOne
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const player = await repo.findOne({
|
||||||
|
where: { name: 'John' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const topPlayer = await repo.findOne({
|
||||||
|
sort: { score: 'desc' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### findMany
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Simple query
|
||||||
|
const players = await repo.findMany({
|
||||||
|
where: { rank: 'gold' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Complex query
|
||||||
|
const players = await repo.findMany({
|
||||||
|
where: {
|
||||||
|
score: { $gte: 100 },
|
||||||
|
rank: { $in: ['gold', 'platinum'] }
|
||||||
|
},
|
||||||
|
sort: { score: 'desc', name: 'asc' },
|
||||||
|
limit: 10,
|
||||||
|
offset: 0
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### findPaginated
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const result = await repo.findPaginated(
|
||||||
|
{ page: 1, pageSize: 20 },
|
||||||
|
{
|
||||||
|
where: { rank: 'gold' },
|
||||||
|
sort: { score: 'desc' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(result.data) // Player[]
|
||||||
|
console.log(result.total) // Total count
|
||||||
|
console.log(result.totalPages) // Total pages
|
||||||
|
console.log(result.hasNext) // Has next page
|
||||||
|
console.log(result.hasPrev) // Has previous page
|
||||||
|
```
|
||||||
|
|
||||||
|
### count
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const count = await repo.count({
|
||||||
|
where: { rank: 'gold' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### exists
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const exists = await repo.exists({
|
||||||
|
where: { email: 'john@example.com' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create Methods
|
||||||
|
|
||||||
|
### create
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const player = await repo.create({
|
||||||
|
name: 'John',
|
||||||
|
score: 0
|
||||||
|
})
|
||||||
|
// Automatically generates id, createdAt, updatedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
### createMany
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const players = await repo.createMany([
|
||||||
|
{ name: 'Alice', score: 100 },
|
||||||
|
{ name: 'Bob', score: 200 },
|
||||||
|
{ name: 'Carol', score: 150 }
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update Methods
|
||||||
|
|
||||||
|
### update
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const updated = await repo.update('player-123', {
|
||||||
|
score: 200,
|
||||||
|
rank: 'gold'
|
||||||
|
})
|
||||||
|
// Automatically updates updatedAt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Delete Methods
|
||||||
|
|
||||||
|
### delete
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Hard delete
|
||||||
|
await repo.delete('player-123')
|
||||||
|
|
||||||
|
// Soft delete (if enabled)
|
||||||
|
// Actually sets the deletedAt field
|
||||||
|
```
|
||||||
|
|
||||||
|
### deleteMany
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const count = await repo.deleteMany({
|
||||||
|
where: { score: { $lt: 10 } }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Soft Delete
|
||||||
|
|
||||||
|
### Enabling Soft Delete
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const repo = createRepository<Player>(mongo, 'players', true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Behavior
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Excludes soft-deleted records by default
|
||||||
|
const players = await repo.findMany()
|
||||||
|
|
||||||
|
// Include soft-deleted records
|
||||||
|
const allPlayers = await repo.findMany({
|
||||||
|
includeSoftDeleted: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Records
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await repo.restore('player-123')
|
||||||
|
```
|
||||||
|
|
||||||
|
## QueryOptions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface QueryOptions<T> {
|
||||||
|
/** Query conditions */
|
||||||
|
where?: WhereCondition<T>
|
||||||
|
|
||||||
|
/** Sorting */
|
||||||
|
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
|
||||||
|
|
||||||
|
/** Limit count */
|
||||||
|
limit?: number
|
||||||
|
|
||||||
|
/** Offset */
|
||||||
|
offset?: number
|
||||||
|
|
||||||
|
/** Include soft-deleted records (only when soft delete is enabled) */
|
||||||
|
includeSoftDeleted?: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PaginatedResult
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface PaginatedResult<T> {
|
||||||
|
data: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalPages: number
|
||||||
|
hasNext: boolean
|
||||||
|
hasPrev: boolean
|
||||||
|
}
|
||||||
|
```
|
||||||
277
docs/src/content/docs/en/modules/database/user.md
Normal file
277
docs/src/content/docs/en/modules/database/user.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
---
|
||||||
|
title: "User Management"
|
||||||
|
description: "UserRepository for user registration, authentication, and role management"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`UserRepository` provides out-of-the-box user management features:
|
||||||
|
|
||||||
|
- User registration and authentication
|
||||||
|
- Password hashing (using scrypt)
|
||||||
|
- Role management
|
||||||
|
- Account status management
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createMongoConnection } from '@esengine/database-drivers'
|
||||||
|
import { UserRepository } from '@esengine/database'
|
||||||
|
|
||||||
|
const mongo = createMongoConnection({
|
||||||
|
uri: 'mongodb://localhost:27017',
|
||||||
|
database: 'game'
|
||||||
|
})
|
||||||
|
await mongo.connect()
|
||||||
|
|
||||||
|
const userRepo = new UserRepository(mongo)
|
||||||
|
```
|
||||||
|
|
||||||
|
## User Registration
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = await userRepo.register({
|
||||||
|
username: 'john',
|
||||||
|
password: 'securePassword123',
|
||||||
|
email: 'john@example.com', // Optional
|
||||||
|
displayName: 'John Doe', // Optional
|
||||||
|
roles: ['player'] // Optional, defaults to []
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(user)
|
||||||
|
// {
|
||||||
|
// id: 'uuid-...',
|
||||||
|
// username: 'john',
|
||||||
|
// email: 'john@example.com',
|
||||||
|
// displayName: 'John Doe',
|
||||||
|
// roles: ['player'],
|
||||||
|
// status: 'active',
|
||||||
|
// createdAt: Date,
|
||||||
|
// updatedAt: Date
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: `register` returns a `SafeUser` which excludes the password hash.
|
||||||
|
|
||||||
|
## User Authentication
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = await userRepo.authenticate('john', 'securePassword123')
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
console.log('Login successful:', user.username)
|
||||||
|
} else {
|
||||||
|
console.log('Invalid username or password')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Management
|
||||||
|
|
||||||
|
### Change Password
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const success = await userRepo.changePassword(
|
||||||
|
userId,
|
||||||
|
'oldPassword123',
|
||||||
|
'newPassword456'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log('Password changed successfully')
|
||||||
|
} else {
|
||||||
|
console.log('Invalid current password')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset Password
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Admin directly resets password
|
||||||
|
const success = await userRepo.resetPassword(userId, 'newPassword123')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Role Management
|
||||||
|
|
||||||
|
### Add Role
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await userRepo.addRole(userId, 'admin')
|
||||||
|
await userRepo.addRole(userId, 'moderator')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Role
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await userRepo.removeRole(userId, 'moderator')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Roles
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Find all admins
|
||||||
|
const admins = await userRepo.findByRole('admin')
|
||||||
|
|
||||||
|
// Check if user has a role
|
||||||
|
const user = await userRepo.findById(userId)
|
||||||
|
const isAdmin = user?.roles.includes('admin')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Querying Users
|
||||||
|
|
||||||
|
### Find by Username
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = await userRepo.findByUsername('john')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find by Email
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const user = await userRepo.findByEmail('john@example.com')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find by Role
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const admins = await userRepo.findByRole('admin')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Inherited Methods
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Paginated query
|
||||||
|
const result = await userRepo.findPaginated(
|
||||||
|
{ page: 1, pageSize: 20 },
|
||||||
|
{
|
||||||
|
where: { status: 'active' },
|
||||||
|
sort: { createdAt: 'desc' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Complex query
|
||||||
|
const users = await userRepo.findMany({
|
||||||
|
where: {
|
||||||
|
status: 'active',
|
||||||
|
roles: { $in: ['admin', 'moderator'] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Account Status
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type UserStatus = 'active' | 'inactive' | 'banned' | 'suspended'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update Status
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await userRepo.update(userId, { status: 'banned' })
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query by Status
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const activeUsers = await userRepo.findMany({
|
||||||
|
where: { status: 'active' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const bannedUsers = await userRepo.findMany({
|
||||||
|
where: { status: 'banned' }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Definitions
|
||||||
|
|
||||||
|
### UserEntity
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface UserEntity extends BaseEntity {
|
||||||
|
username: string
|
||||||
|
passwordHash: string
|
||||||
|
email?: string
|
||||||
|
displayName?: string
|
||||||
|
roles: string[]
|
||||||
|
status: UserStatus
|
||||||
|
lastLoginAt?: Date
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SafeUser
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type SafeUser = Omit<UserEntity, 'passwordHash'>
|
||||||
|
```
|
||||||
|
|
||||||
|
### CreateUserParams
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CreateUserParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
email?: string
|
||||||
|
displayName?: string
|
||||||
|
roles?: string[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Password Utilities
|
||||||
|
|
||||||
|
Standalone password utility functions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { hashPassword, verifyPassword } from '@esengine/database'
|
||||||
|
|
||||||
|
// Hash password
|
||||||
|
const hash = await hashPassword('myPassword123')
|
||||||
|
|
||||||
|
// Verify password
|
||||||
|
const isValid = await verifyPassword('myPassword123', hash)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
|
||||||
|
- Uses Node.js built-in `scrypt` algorithm
|
||||||
|
- Automatically generates random salt
|
||||||
|
- Uses secure iteration parameters by default
|
||||||
|
- Hash format: `salt:hash` (both hex encoded)
|
||||||
|
|
||||||
|
## Extending UserRepository
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { UserRepository, UserEntity } from '@esengine/database'
|
||||||
|
|
||||||
|
interface GameUser extends UserEntity {
|
||||||
|
level: number
|
||||||
|
experience: number
|
||||||
|
coins: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class GameUserRepository extends UserRepository {
|
||||||
|
// Override collection name
|
||||||
|
constructor(connection: IMongoConnection) {
|
||||||
|
super(connection, 'game_users')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add game-related methods
|
||||||
|
async addExperience(userId: string, amount: number): Promise<GameUser | null> {
|
||||||
|
const user = await this.findById(userId) as GameUser | null
|
||||||
|
if (!user) return null
|
||||||
|
|
||||||
|
const newExp = user.experience + amount
|
||||||
|
const newLevel = Math.floor(newExp / 1000) + 1
|
||||||
|
|
||||||
|
return this.update(userId, {
|
||||||
|
experience: newExp,
|
||||||
|
level: newLevel
|
||||||
|
}) as Promise<GameUser | null>
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTopPlayers(limit: number = 10): Promise<GameUser[]> {
|
||||||
|
return this.findMany({
|
||||||
|
sort: { level: 'desc', experience: 'desc' },
|
||||||
|
limit
|
||||||
|
}) as Promise<GameUser[]>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user