init: 从 cs-hand/hand-server-2/lib/shrimp 抽出独立仓
- 6 个框架源文件搬到 src/, 加 src/index.ts barrel 作为公共 API 入口 - package.json 标 private, main/types 指 src/index.ts (ts-node 消费方编译) - 加 vitest + 22 条单元测试 (ILabel / MemoryShrimpCrud) 全通过 - examples/basic-admin/ 用 MemoryShrimpCrud 跑零 Mongo 依赖 demo 待办: ShrimpServer 强耦合 Mongo+JWT+Express, crudRegistry 用 .model 判断数据源, 这两个问题在 demo 中已暴露, 计划后续重构补到 90 分。
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { Types, Model, FilterQuery } from 'mongoose';
|
||||
import { ShrimpCrud } from './Shrimp';
|
||||
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
|
||||
|
||||
/** 判断值是否为 IShrimpCondition 对象 */
|
||||
private isCondition = isShrimpCondition
|
||||
|
||||
/** 将 IShrimpCondition 翻译为 MongoDB 查询片段 */
|
||||
private translateCondition(cond: IShrimpCondition | any): any {
|
||||
if (!this.isCondition(cond)) return cond // 简写: 原始值 = eq
|
||||
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
|
||||
}
|
||||
|
||||
/** 将 IShrimpQuery 翻译为 Mongoose 查询 */
|
||||
private buildMongooseQuery(query?: IShrimpQuery): { filter: FilterQuery<T>, sort: any, skip: number, limit: number } {
|
||||
const filter: FilterQuery<T> = {}
|
||||
|
||||
// where → AND 条件
|
||||
if (query?.where) {
|
||||
for (const [key, cond] of Object.entries(query.where)) {
|
||||
(filter as any)[key] = this.translateCondition(cond)
|
||||
}
|
||||
}
|
||||
|
||||
// orWhere → $or
|
||||
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) {
|
||||
// 优先用 getRelatedModel,其次用 crudRegistry(自动解析 relation)
|
||||
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