让只用 Shrimp 核心(RPC + 模块声明 + 内存 CRUD)的项目不必装
express / mongoose / bcrypt / jsonwebtoken,显著降低 install 时间。
- src/core.ts:0 重依赖入口(Shrimp / MemoryShrimpCrud / 类型)
- src/server.ts:在 core 之上再导出 ShrimpServer + MongoShrimpCrud
- package.json:加 exports 字段 + peerDependenciesMeta.optional
- Shrimp.ts:bcrypt 改 lazy import,只有走 Password 字段才触发
- MemoryShrimpCrud.populateItems:接 crudRegistry 参数(对齐 Mongo)
- ShrimpTableModule 构造时自动登记 crudRegistry,业务无需手动 wire
- 新增 test/crudRegistry.test.ts 验证自动登记 + relation 自动 populate
BREAKING: import { ShrimpServer } from 'shrimp-server' 不再可用,
请改为 'shrimp-server/server'。
127 lines
4.5 KiB
TypeScript
127 lines
4.5 KiB
TypeScript
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
|
|
|
|
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[], crudRegistry?: Map<string, ShrimpCrud<any>>): Promise<void> {
|
|
if (!crudRegistry) return
|
|
for (const pop of populates) {
|
|
const relatedCrud = crudRegistry.get(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
|
|
}
|
|
}
|