Files
shrimp-server/src/MongoShrimpCrud.ts
T
wc24 8548a1760a 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 分。
2026-05-23 15:07:55 +08:00

131 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}