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,127 @@
|
||||
import { ShrimpCrud } from './Shrimp'
|
||||
import { IShrimpQuery, IShrimpCondition, IShrimpPopulate, IPageResult, isShrimpCondition } from './IShrimpType'
|
||||
|
||||
export class MemoryShrimpCrud<T extends { _id: string }> implements ShrimpCrud<T> {
|
||||
private data: Map<string, T> = new Map()
|
||||
private autoId: number = 1
|
||||
/** 获取关联模块的数据源(用于 populate) */
|
||||
getRelatedCrud?: (moduleKey: string) => ShrimpCrud<any> | undefined
|
||||
|
||||
private isCondition = isShrimpCondition
|
||||
|
||||
private matchCondition(item: any, key: string, cond: IShrimpCondition | any): boolean {
|
||||
const v = item[key]
|
||||
if (this.isCondition(cond)) {
|
||||
if (cond.eq !== undefined && v !== cond.eq) return false
|
||||
if (cond.in !== undefined && !cond.in.includes(v)) return false
|
||||
if (cond.like !== undefined && !String(v ?? '').toLowerCase().includes(String(cond.like).toLowerCase())) return false
|
||||
if (cond.gte !== undefined && v < cond.gte) return false
|
||||
if (cond.lte !== undefined && v > cond.lte) return false
|
||||
return true
|
||||
}
|
||||
return v === cond
|
||||
}
|
||||
|
||||
async findOne(query?: any): Promise<T | null> {
|
||||
if (!query) return null
|
||||
for (const item of this.data.values()) {
|
||||
if (Object.keys(query).every(key => (item as any)[key] === query[key])) {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async list(query?: IShrimpQuery): Promise<IPageResult<T>> {
|
||||
let items = Array.from(this.data.values())
|
||||
|
||||
// where 条件 (AND)
|
||||
if (query?.where) {
|
||||
for (const [key, cond] of Object.entries(query.where)) {
|
||||
items = items.filter(item => this.matchCondition(item, key, cond))
|
||||
}
|
||||
}
|
||||
|
||||
// orWhere 条件 (OR 组之间)
|
||||
if (query?.orWhere && query.orWhere.length > 0) {
|
||||
items = items.filter(item =>
|
||||
query.orWhere!.some(group =>
|
||||
Object.entries(group).every(([key, cond]) => this.matchCondition(item, key, cond))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const total = items.length
|
||||
|
||||
// 排序
|
||||
if (query?.order) {
|
||||
for (const [key, dir] of query.order) {
|
||||
const d = dir === 'desc' ? -1 : 1
|
||||
items.sort((a, b) => {
|
||||
const va = (a as any)[key], vb = (b as any)[key]
|
||||
if (va == null && vb == null) return 0
|
||||
if (va == null) return 1
|
||||
if (vb == null) return -1
|
||||
if (va < vb) return -d
|
||||
if (va > vb) return d
|
||||
return 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 分页
|
||||
const offset = query?.offset || 0
|
||||
const limit = query?.limit ?? items.length
|
||||
const list = items.slice(offset, offset + limit)
|
||||
const page = Math.floor(offset / limit) + 1
|
||||
const pageSize = limit
|
||||
const totalPages = Math.ceil(total / pageSize)
|
||||
|
||||
return { list, total, page, pageSize, totalPages }
|
||||
}
|
||||
|
||||
async populateItems(items: any[], populates: IShrimpPopulate[]): Promise<void> {
|
||||
for (const pop of populates) {
|
||||
const relatedCrud = this.getRelatedCrud?.(pop.moduleKey)
|
||||
if (!relatedCrud) continue
|
||||
const allRelated = (await relatedCrud.list({})).list
|
||||
const labelMap = new Map(allRelated.map(item => [String(item._id), (item 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 get(id: string): Promise<T | null> {
|
||||
return this.data.get(id) || null
|
||||
}
|
||||
|
||||
async add(item: Omit<T, '_id'>): Promise<T> {
|
||||
const id = (this.autoId++).toString()
|
||||
const newItem = { _id: id, ...item } as T
|
||||
this.data.set(id, newItem)
|
||||
return newItem
|
||||
}
|
||||
|
||||
async edit(id: string, item: Partial<T>): Promise<T | null> {
|
||||
const old = this.data.get(id)
|
||||
if (!old) return null
|
||||
const updated = { ...old, ...item }
|
||||
this.data.set(id, updated)
|
||||
return updated
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
return this.data.delete(id)
|
||||
}
|
||||
|
||||
async clear(): Promise<boolean> {
|
||||
this.data.clear()
|
||||
return true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user