v0.5.0: 重新组织目录结构,按职责拆分
- Shrimp.ts (875 行) 按类拆分到 Shrimp/(7 文件: Shrimp, ShrimpModule, ShrimpTableModule, ShrimpPower, ShrimpCrud, UserRoleModule, OperationLogModule) - forMongo/: MongoShrimpCrud + MongoShrimp(原 ShrimpServer 的 mongo 装配部分,无 express) - forExpress/: buildShrimpRouter + jwtAuth + types - main/: index(0 依赖核心) + expmon(express+mongo 整合,导出 ShrimpServer) + types(纯协议) - 入口子路径 server → expmon(express+mongo 整合方案命名更准确) - 删除冗余的 index.ts / core.ts(合并为 main/index.ts)
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* MongoShrimp — Shrimp + Mongoose 装配体(无 HTTP 层)
|
||||
*
|
||||
* 提供:
|
||||
* - 默认 Mongoose Schema(User / Role / OperationLog),子类可覆盖
|
||||
* - Mongoose Model 工厂 + 缓存
|
||||
* - 默认 CRUD / Module 工厂,绑定 MongoShrimpCrud
|
||||
* - mongoose.connect + onDbConnected 钩子
|
||||
* - initDefaultData 默认角色/用户播种
|
||||
*
|
||||
* 不知道:express / jwt / 任何 HTTP 概念。
|
||||
* 上层把 HTTP 装到这之上,见 main/expmon.ts 的 ShrimpServer。
|
||||
*/
|
||||
|
||||
import mongoose, { Schema, Model } from "mongoose"
|
||||
import { Shrimp } from "../Shrimp/Shrimp"
|
||||
import { UserModule, RoleModule } from "../Shrimp/UserRoleModule"
|
||||
import { OperationLogModule } from "../Shrimp/OperationLogModule"
|
||||
import type { ShrimpCrud } from "../Shrimp/ShrimpCrud"
|
||||
import { IUser, IRole, IOperationLog, WithId } from "../IShrimpType"
|
||||
import { MongoShrimpCrud } from "./MongoShrimpCrud"
|
||||
|
||||
export const UserSchema = new Schema({
|
||||
username: { type: String, required: true },
|
||||
password: { type: String, required: true },
|
||||
nickname: { type: String, default: "" },
|
||||
roleId: { type: [Schema.Types.ObjectId], ref: "Role", default: [] },
|
||||
active: { type: Boolean, default: true },
|
||||
}, { timestamps: true })
|
||||
|
||||
export const RoleSchema = new Schema({
|
||||
name: { type: String, required: true },
|
||||
description: { type: String, default: "" },
|
||||
isSuperAdmin: { type: Boolean, default: false },
|
||||
powers: { type: [String], default: [] },
|
||||
}, { timestamps: true })
|
||||
|
||||
export const OperationLogSchema = new Schema({
|
||||
operatorId: { type: String, default: null },
|
||||
ip: { type: String, default: "" },
|
||||
method: { type: String, required: true },
|
||||
args: { type: String, default: "" },
|
||||
success: { type: Boolean, default: true },
|
||||
errorMsg: { type: String, default: "" },
|
||||
createdAt: { type: Number, default: () => Date.now() },
|
||||
}, { timestamps: false })
|
||||
|
||||
export class MongoShrimp extends Shrimp {
|
||||
userModule: UserModule
|
||||
roleModule: RoleModule
|
||||
operationLogModule: OperationLogModule
|
||||
|
||||
protected _userModel?: Model<IUser>
|
||||
protected _roleModel?: Model<IRole>
|
||||
protected _operationLogModel?: Model<IOperationLog>
|
||||
|
||||
constructor(mongodbUri: string = "mongodb://localhost:27017/shrimp") {
|
||||
super()
|
||||
|
||||
const userCrud = this.createUserCrud()
|
||||
const roleCrud = this.createRoleCrud()
|
||||
const operationLogCrud = this.createOperationLogCrud()
|
||||
|
||||
this.userModule = this.createUserModule(userCrud)
|
||||
this.roleModule = this.createRoleModule(roleCrud)
|
||||
this.operationLogModule = this.createOperationLogModule(operationLogCrud)
|
||||
|
||||
mongoose.connect(mongodbUri).then(() => {
|
||||
console.log(mongodbUri + " MongoDB connected")
|
||||
this.onDbConnected()
|
||||
})
|
||||
}
|
||||
|
||||
// ============== 可覆盖的 Schema 工厂 ==============
|
||||
|
||||
protected createUserSchema(): mongoose.Schema {
|
||||
return UserSchema
|
||||
}
|
||||
|
||||
protected createRoleSchema(): mongoose.Schema {
|
||||
return RoleSchema
|
||||
}
|
||||
|
||||
protected createOperationLogSchema(): mongoose.Schema {
|
||||
return OperationLogSchema
|
||||
}
|
||||
|
||||
protected getUserModel(): Model<IUser> {
|
||||
if (!this._userModel) {
|
||||
this._userModel = mongoose.model<IUser>("shrimp_users", this.createUserSchema())
|
||||
}
|
||||
return this._userModel
|
||||
}
|
||||
|
||||
protected getRoleModel(): Model<IRole> {
|
||||
if (!this._roleModel) {
|
||||
this._roleModel = mongoose.model<IRole>("shrimp_roles", this.createRoleSchema())
|
||||
}
|
||||
return this._roleModel
|
||||
}
|
||||
|
||||
protected getOperationLogModel(): Model<IOperationLog> {
|
||||
if (!this._operationLogModel) {
|
||||
this._operationLogModel = mongoose.model<IOperationLog>("shrimp_operation_logs", this.createOperationLogSchema())
|
||||
}
|
||||
return this._operationLogModel
|
||||
}
|
||||
|
||||
// ============== 可覆盖的 Crud 工厂 ==============
|
||||
|
||||
protected createUserCrud(): MongoShrimpCrud<WithId<IUser>> {
|
||||
return new MongoShrimpCrud<WithId<IUser>>(this.getUserModel())
|
||||
}
|
||||
|
||||
protected createRoleCrud(): MongoShrimpCrud<WithId<IRole>> {
|
||||
return new MongoShrimpCrud<WithId<IRole>>(this.getRoleModel())
|
||||
}
|
||||
|
||||
protected createOperationLogCrud(): MongoShrimpCrud<WithId<IOperationLog>> {
|
||||
return new MongoShrimpCrud<WithId<IOperationLog>>(this.getOperationLogModel())
|
||||
}
|
||||
|
||||
// ============== 可覆盖的 Module 工厂 ==============
|
||||
|
||||
protected createUserModule(dataSource: ShrimpCrud<WithId<IUser>>): UserModule {
|
||||
return new UserModule(this, dataSource)
|
||||
}
|
||||
|
||||
protected createRoleModule(dataSource: ShrimpCrud<WithId<IRole>>): RoleModule {
|
||||
return new RoleModule(this, dataSource)
|
||||
}
|
||||
|
||||
protected createOperationLogModule(dataSource: ShrimpCrud<WithId<IOperationLog>>): OperationLogModule {
|
||||
return new OperationLogModule(this.userModule, dataSource)
|
||||
}
|
||||
|
||||
// ============== 启动钩子 ==============
|
||||
|
||||
protected async onDbConnected(): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
/** 初始化默认数据(如默认角色、用户),子类可覆盖 */
|
||||
protected async initDefaultData(name: string = "Administrator", description: string = "System administrator with all permissions"): Promise<void> {
|
||||
const result = await this.userModule.dataSource.list({ limit: 1 })
|
||||
if (result.total > 0) return
|
||||
console.log("初始化默认数据")
|
||||
const adminRole = await this.roleModule.dataSource.add({
|
||||
name: name,
|
||||
description: description,
|
||||
isSuperAdmin: true,
|
||||
powers: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
|
||||
await this.userModule.add({
|
||||
username: "admin",
|
||||
password: "admin",
|
||||
nickname: "Administrator",
|
||||
roleId: [adminRole._id],
|
||||
active: true,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Model, FilterQuery } from 'mongoose'
|
||||
import type { ShrimpCrud } from "../Shrimp/ShrimpCrud"
|
||||
import { IShrimpQuery, IShrimpCondition, IShrimpPopulate, IPageResult, ShrimpConfig, isShrimpCondition } from "../IShrimpType"
|
||||
|
||||
export class MongoShrimpCrud<T> implements ShrimpCrud<T> {
|
||||
constructor(private model: Model<any>) { }
|
||||
|
||||
/** 获取关联模块的 Mongoose Model(用于 populate / $lookup) */
|
||||
getRelatedModel?: (moduleKey: string) => Model<any> | undefined
|
||||
|
||||
private isCondition = isShrimpCondition
|
||||
|
||||
private translateCondition(cond: IShrimpCondition | any): any {
|
||||
if (!this.isCondition(cond)) return cond
|
||||
const q: any = {}
|
||||
if (cond.eq !== undefined) q.$eq = cond.eq
|
||||
if (cond.in !== undefined) q.$in = cond.in
|
||||
if (cond.like !== undefined) q.$regex = cond.like, q.$options = 'i'
|
||||
if (cond.gte !== undefined) q.$gte = cond.gte
|
||||
if (cond.lte !== undefined) q.$lte = cond.lte
|
||||
return q
|
||||
}
|
||||
|
||||
private buildMongooseQuery(query?: IShrimpQuery): { filter: FilterQuery<T>, sort: any, skip: number, limit: number } {
|
||||
const filter: FilterQuery<T> = {}
|
||||
|
||||
if (query?.where) {
|
||||
for (const [key, cond] of Object.entries(query.where)) {
|
||||
(filter as any)[key] = this.translateCondition(cond)
|
||||
}
|
||||
}
|
||||
|
||||
if (query?.orWhere && query.orWhere.length > 0) {
|
||||
filter.$or = query.orWhere.map(group => {
|
||||
const obj: any = {}
|
||||
for (const [key, cond] of Object.entries(group)) {
|
||||
obj[key] = this.translateCondition(cond)
|
||||
}
|
||||
return obj
|
||||
})
|
||||
}
|
||||
|
||||
const sort: any = {}
|
||||
if (query?.order) {
|
||||
for (const [key, dir] of query.order) {
|
||||
sort[key] = dir === 'desc' ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filter,
|
||||
sort,
|
||||
skip: query?.offset || 0,
|
||||
limit: query?.limit ?? ShrimpConfig.defaultPageSize,
|
||||
}
|
||||
}
|
||||
|
||||
async list(query?: IShrimpQuery): Promise<IPageResult<T>> {
|
||||
const { filter, sort, skip, limit } = this.buildMongooseQuery(query)
|
||||
|
||||
const [docs, total] = await Promise.all([
|
||||
this.model.find(filter).sort(sort).skip(skip).limit(limit).lean(),
|
||||
this.model.countDocuments(filter),
|
||||
])
|
||||
const page = Math.floor(skip / limit) + 1
|
||||
const totalPages = Math.ceil(total / limit)
|
||||
return { list: docs, total, page, pageSize: limit, totalPages }
|
||||
}
|
||||
|
||||
async populateItems(items: any[], populates: IShrimpPopulate[], crudRegistry?: Map<string, any>): Promise<void> {
|
||||
for (const pop of populates) {
|
||||
let relatedModel = this.getRelatedModel?.(pop.moduleKey)
|
||||
if (!relatedModel && crudRegistry) {
|
||||
const relatedCrud = crudRegistry.get(pop.moduleKey) as any
|
||||
relatedModel = relatedCrud?.model
|
||||
}
|
||||
if (!relatedModel) continue
|
||||
|
||||
const allRefIds = new Set<string>()
|
||||
for (const item of items) {
|
||||
const val = (item as any)[pop.key]
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((id: any) => { if (id != null) allRefIds.add(String(id)) })
|
||||
} else if (val != null) {
|
||||
allRefIds.add(String(val))
|
||||
}
|
||||
}
|
||||
if (allRefIds.size === 0) continue
|
||||
const docs = await relatedModel.find({ _id: { $in: Array.from(allRefIds) } }).select(`${pop.labelKey} _id`)
|
||||
const labelMap = new Map(docs.map(doc => [String(doc._id), (doc as any)[pop.labelKey]]))
|
||||
for (const item of items) {
|
||||
const val = (item as any)[pop.key]
|
||||
if (Array.isArray(val)) {
|
||||
(item as any)[`${pop.key}_label`] = val.map((id: any) => labelMap.get(String(id)) ?? null)
|
||||
} else if (val != null) {
|
||||
(item as any)[`${pop.key}_label`] = labelMap.get(String(val)) ?? null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async findOne(query?: any): Promise<T | null> {
|
||||
return await this.model.findOne(query || {}).lean() as T | null
|
||||
}
|
||||
|
||||
async get(id: string): Promise<T | null> {
|
||||
return await this.model.findById(id).lean() as T | null
|
||||
}
|
||||
async add(item: Omit<T, '_id'>): Promise<T> {
|
||||
return await this.model.create(item)
|
||||
}
|
||||
async edit(id: string, item: Partial<T>): Promise<T | null> {
|
||||
return await this.model.findByIdAndUpdate(id, item, { new: true })
|
||||
}
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const result = await this.model.findByIdAndDelete(id)
|
||||
return !!result
|
||||
}
|
||||
async clear(): Promise<boolean> {
|
||||
await this.model.deleteMany({})
|
||||
return true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user