- 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)
127 lines
4.7 KiB
TypeScript
127 lines
4.7 KiB
TypeScript
import { AsyncLocalStorage } from 'async_hooks'
|
|
import {
|
|
ShrimpCode, IShrimpResponse, IModuleInfo, IShrimpOperator, IMenuItem, ILabel,
|
|
} from "../IShrimpType"
|
|
import { IPasswordHasher, bcryptHasher } from '../PasswordHasher'
|
|
import type { ShrimpModule } from "./ShrimpModule"
|
|
import type { ShrimpCrud } from "./ShrimpCrud"
|
|
|
|
export class Shrimp {
|
|
root: Shrimp = this
|
|
modules: Map<string, ShrimpModule> = new Map()
|
|
/** 全局 CRUD 注册表,用于自动解析 relation */
|
|
crudRegistry: Map<string, ShrimpCrud<any>> = new Map()
|
|
/**
|
|
* 密码哈希器,默认 bcryptHasher(lazy import bcrypt)。
|
|
* 消费方可在启动时替换:`Shrimp.passwordHasher = plainHasher`
|
|
*/
|
|
static passwordHasher: IPasswordHasher = bcryptHasher
|
|
/** 请求级操作者上下文 */
|
|
static asyncLocalStorage = new AsyncLocalStorage<IShrimpOperator | null>()
|
|
get operator(): IShrimpOperator | null {
|
|
return Shrimp.asyncLocalStorage.getStore() ?? null
|
|
}
|
|
async call(path: string[], ...args: any[]): Promise<any> {
|
|
let module = this.modules.get(path[0])
|
|
if (module == null) {
|
|
throw new Error(`Module "${path[0]}" not found`)
|
|
}
|
|
return await module.call(path.slice(1), ...args)
|
|
}
|
|
checkEvent(powers: Set<string>, path: string[]): boolean {
|
|
let module = this.modules.get(path[0])
|
|
if (module == null) return false
|
|
return module.checkEvent(powers, path.slice(1))
|
|
}
|
|
getModuleInfo(powers: Set<string>): IModuleInfo[] {
|
|
const list: IModuleInfo[] = []
|
|
this.collectModuleInfo(this.modules, powers, undefined, list)
|
|
return list
|
|
}
|
|
|
|
private collectModuleInfo(modules: Map<string, ShrimpModule>, powers: Set<string>, parent: string | undefined, out: IModuleInfo[]) {
|
|
modules.forEach(module => {
|
|
if (!powers.has(module.power.key)) return
|
|
const info: IModuleInfo = {
|
|
moduleKey: module.moduleKey,
|
|
events: module.getEventInfo(powers),
|
|
label: module.label,
|
|
fields: module.getFields(),
|
|
}
|
|
if (module.icon) info.icon = module.icon
|
|
if (parent) info.parent = parent
|
|
out.push(info)
|
|
if (module.modules.size > 0) {
|
|
this.collectModuleInfo(module.modules, powers, module.moduleKey, out)
|
|
}
|
|
})
|
|
}
|
|
|
|
success<T>(data: T, tip?: ILabel, log?: string): IShrimpResponse<T> {
|
|
const r: IShrimpResponse<T> = { code: ShrimpCode.OK, data }
|
|
if (tip) r.tip = tip
|
|
if (log) r.log = log
|
|
return r
|
|
}
|
|
|
|
fail<T = any>(code: ShrimpCode, tip?: ILabel, log?: string): IShrimpResponse<T> {
|
|
const r: IShrimpResponse<T> = { code }
|
|
if (tip) r.tip = tip
|
|
if (log) r.log = log
|
|
return r
|
|
}
|
|
|
|
async allPower(): Promise<{ key: string; label: ILabel }[]> {
|
|
const powers: { key: string; label: ILabel }[] = []
|
|
|
|
const collect = (module: any) => {
|
|
powers.push({ key: module.power.key, label: module.power.label })
|
|
powers.push(...module.power.children)
|
|
module.modules.forEach(collect)
|
|
}
|
|
|
|
this.root.modules.forEach(collect)
|
|
|
|
const map = new Map<string, ILabel>()
|
|
powers.forEach(p => map.set(p.key, p.label))
|
|
return Array.from(map.entries()).map(([key, label]) => ({ key, label }))
|
|
}
|
|
|
|
/** 菜单树,默认平铺所有模块;子类可覆盖为分组结构 */
|
|
getMenuTree(): IMenuItem[] {
|
|
const items: IMenuItem[] = []
|
|
this.modules.forEach(module => {
|
|
items.push({
|
|
name: module.moduleKey,
|
|
label: module.label,
|
|
moduleKey: module.moduleKey
|
|
})
|
|
})
|
|
return items
|
|
}
|
|
|
|
/** 根据权限过滤菜单树 */
|
|
filterMenuTree(fullMenuTree: IMenuItem[], rolePower: string[]): IMenuItem[] {
|
|
const allowedModules = new Set(rolePower.map(p => p.split('.')[0]))
|
|
const filter = (nodes: IMenuItem[]): IMenuItem[] => {
|
|
const result: IMenuItem[] = []
|
|
for (const node of nodes) {
|
|
if (node.moduleKey) {
|
|
if (allowedModules.has(node.moduleKey)) {
|
|
result.push(node)
|
|
}
|
|
continue
|
|
}
|
|
if (!node.children || node.children.length === 0) {
|
|
continue
|
|
}
|
|
const filteredChildren = filter(node.children)
|
|
if (filteredChildren.length === 0) continue
|
|
result.push({ ...node, children: filteredChildren })
|
|
}
|
|
return result
|
|
}
|
|
return filter(fullMenuTree)
|
|
}
|
|
}
|