feat!: 拆分 core / server 入口,peer deps 全部 optional
让只用 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'。
This commit is contained in:
@@ -16,19 +16,30 @@ cd examples/basic-admin && npm install && npm start
|
||||
|
||||
```sh
|
||||
# 推荐: HTTP(任何能访问 git.zeromvc.com 的机器都能用)
|
||||
npm install git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.1.0
|
||||
npm install git+http://git.zeromvc.com/wc24/shrimp-server.git#v0.2.0
|
||||
|
||||
# 也可以 SSH(需要 ssh key 配好)
|
||||
npm install git+ssh://git@git.zeromvc.com:wc24/shrimp-server.git#v0.1.0
|
||||
npm install git+ssh://git@git.zeromvc.com:wc24/shrimp-server.git#v0.2.0
|
||||
```
|
||||
|
||||
> 仓库 ship `.ts` 源码,消费方项目需要 TS 运行器(`tsx` / `ts-node`)或在打包工具(vite / webpack / esbuild)里能解析 `.ts`。
|
||||
|
||||
### 两个入口,按需选择
|
||||
|
||||
包内提供了**核心**和**完整 server** 两个 entry,**peer deps 全部 optional**——你只装自己用得到的那一份:
|
||||
|
||||
```ts
|
||||
import { ShrimpServer, ShrimpTableModule, MongoShrimpCrud, ShrimpFieldTypes } from "shrimp-server"
|
||||
// ↓ 详见下面"想做什么"指南
|
||||
// 1) 核心: 0 重依赖(只需 node + tsx),适合自己接 HTTP / 只用内存 CRUD / 嵌入其它框架
|
||||
import { Shrimp, ShrimpTableModule, MemoryShrimpCrud, ShrimpFieldTypes } from "shrimp-server"
|
||||
|
||||
// 2) 完整 server: 自带 Express Router + Mongoose + JWT,需要装上面 4 个 peer
|
||||
import { ShrimpServer, MongoShrimpCrud } from "shrimp-server/server"
|
||||
```
|
||||
|
||||
只用 (1) 时,**不要**在自己的 package.json 里写 express/mongoose/bcrypt/jsonwebtoken,可以省下大量 install 时间(尤其是 mongoose 的 native binding)。需要 (2) 时按 peer 范围装齐即可。
|
||||
|
||||
> bcrypt 在 core 里走的是动态 `import('bcrypt')`,不触发 Password 字段 hash / `UserModule.verifyPassword` 就完全不会去 require 它。所以即便用 core 也注册了 password 字段,只要你不真正调用 `add/edit` 写入 Password 或 `getUser` 校验密码,bcrypt 仍是可不装的。
|
||||
|
||||
---
|
||||
|
||||
# 使用指南
|
||||
|
||||
@@ -61,16 +61,10 @@ const superAdmin: IShrimpOperator = {
|
||||
async function bootstrap() {
|
||||
const shrimp = new Shrimp()
|
||||
|
||||
// TODO(70→90): ShrimpTableModule 当前只在 dataSource.model 存在时自动登记 crudRegistry,
|
||||
// 这里需要手动 wire 让 MemoryShrimpCrud 的 populate 也能查到关联表
|
||||
const tagCrud = new MemoryShrimpCrud<WithId<ITag>>()
|
||||
tagCrud.getRelatedCrud = (k) => shrimp.crudRegistry.get(k)
|
||||
shrimp.crudRegistry.set("tag", tagCrud)
|
||||
new ShrimpTableModule(shrimp, "tag", tagCrud, tagFields, "标签管理").initCrud()
|
||||
|
||||
const productCrud = new MemoryShrimpCrud<WithId<IProduct>>()
|
||||
productCrud.getRelatedCrud = (k) => shrimp.crudRegistry.get(k)
|
||||
shrimp.crudRegistry.set("product", productCrud)
|
||||
new ShrimpTableModule(shrimp, "product", productCrud, productFields, "商品管理").initCrud()
|
||||
|
||||
// 种子数据
|
||||
|
||||
+18
-1
@@ -1,10 +1,21 @@
|
||||
{
|
||||
"name": "shrimp-server",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"description": "Shrimp 框架后端 — 声明式字段驱动的 Node.js 管理后台框架(Express + Mongoose + JWT)",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/core.ts",
|
||||
"default": "./src/core.ts"
|
||||
},
|
||||
"./server": {
|
||||
"types": "./src/server.ts",
|
||||
"default": "./src/server.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"README.md"
|
||||
@@ -21,6 +32,12 @@
|
||||
"jsonwebtoken": "^8 || ^9",
|
||||
"bcrypt": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"express": { "optional": true },
|
||||
"mongoose": { "optional": true },
|
||||
"jsonwebtoken": { "optional": true },
|
||||
"bcrypt": { "optional": true }
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.0",
|
||||
|
||||
@@ -4,8 +4,6 @@ import { IShrimpQuery, IShrimpCondition, IShrimpPopulate, IPageResult, isShrimpC
|
||||
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
|
||||
|
||||
@@ -80,9 +78,10 @@ export class MemoryShrimpCrud<T extends { _id: string }> implements ShrimpCrud<T
|
||||
return { list, total, page, pageSize, totalPages }
|
||||
}
|
||||
|
||||
async populateItems(items: any[], populates: IShrimpPopulate[]): Promise<void> {
|
||||
async populateItems(items: any[], populates: IShrimpPopulate[], crudRegistry?: Map<string, ShrimpCrud<any>>): Promise<void> {
|
||||
if (!crudRegistry) return
|
||||
for (const pop of populates) {
|
||||
const relatedCrud = this.getRelatedCrud?.(pop.moduleKey)
|
||||
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]]))
|
||||
|
||||
+10
-5
@@ -5,9 +5,14 @@
|
||||
|
||||
import { ShrimpCode, IShrimpResponse, ITableCrudProto, IModuleInfo, IUser, IRole, WithId, IRoleProto, IPageQuery, IPageResult, IFieldConfig, IFieldRule, IFieldTypeHandler, IShrimpQuery, IShrimpCondition, IShrimpPopulate, ShrimpConfig, IShrimpOperator, IMenuItem, ShrimpError, ShrimpFieldTypes, ILabel } from "./IShrimpType"
|
||||
|
||||
import * as bcrypt from "bcrypt"
|
||||
import { AsyncLocalStorage } from 'async_hooks'
|
||||
|
||||
// 懒加载 bcrypt: 只有走到 Password 字段 hash 或 verifyPassword 才 import,
|
||||
// 让纯核心(MemoryShrimpCrud + 业务模块)的消费方可以不装 bcrypt
|
||||
async function loadBcrypt(): Promise<typeof import("bcrypt")> {
|
||||
return await import("bcrypt")
|
||||
}
|
||||
|
||||
export class Shrimp {
|
||||
root: Shrimp = this
|
||||
modules: Map<string, ShrimpModule> = new Map()
|
||||
@@ -253,6 +258,7 @@ export const defaultTypeHandlers: Record<string, IFieldTypeHandler> = {
|
||||
[ShrimpFieldTypes.Password]: {
|
||||
reqFilter: async (value) => {
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
const bcrypt = await loadBcrypt()
|
||||
return bcrypt.hash(value, 10)
|
||||
}
|
||||
return value
|
||||
@@ -318,10 +324,8 @@ export class ShrimpTableModule<T, P extends ITableCrudProto<T> = ITableCrudProto
|
||||
if (fields) {
|
||||
this.fields = fields
|
||||
}
|
||||
// 自动注册 CRUD 到 root.crudRegistry,供 MongoShrimpCrud.populateItems 自动解析 relation
|
||||
if ((dataSource as any).model) {
|
||||
this.root.crudRegistry.set(moduleKey, dataSource)
|
||||
}
|
||||
// 自动注册 CRUD 到 root.crudRegistry,populateItems 据此解析 relation
|
||||
this.root.crudRegistry.set(moduleKey, dataSource)
|
||||
}
|
||||
|
||||
getFields(): IFieldConfig[] {
|
||||
@@ -711,6 +715,7 @@ export class UserModule extends ShrimpTableModule<WithId<IUser>> {
|
||||
|
||||
/** 校验密码,子类可覆盖 */
|
||||
protected async verifyPassword(storedPassword: string, plainPassword: string): Promise<boolean> {
|
||||
const bcrypt = await loadBcrypt()
|
||||
return bcrypt.compare(plainPassword, storedPassword)
|
||||
}
|
||||
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Shrimp 核心入口 — 0 重依赖
|
||||
*
|
||||
* 只暴露 Shrimp 框架本体 + 内存数据源 + 类型定义,不引入
|
||||
* express / mongoose / jsonwebtoken。bcrypt 是 lazy import,
|
||||
* 不走 Password 字段就不会触发。
|
||||
*
|
||||
* 适用场景:
|
||||
* - 只想用 Shrimp 的 RPC dispatch + 模块/字段声明 + 内存 CRUD
|
||||
* - 自己接 HTTP / 自己写存储层
|
||||
* - 跑测试 / 跑无 DB 的 demo
|
||||
*
|
||||
* 需要完整后台(Express + Mongoose + JWT)请从 'shrimp-server/server' 导入。
|
||||
*/
|
||||
|
||||
// ===== 类型 / 接口 / 枚举 =====
|
||||
export {
|
||||
ShrimpCode,
|
||||
ShrimpError,
|
||||
ShrimpFieldTypes,
|
||||
ShrimpConfig,
|
||||
isShrimpCondition,
|
||||
} from "./IShrimpType"
|
||||
|
||||
export type {
|
||||
ILabel,
|
||||
IShrimpResponse,
|
||||
IModuleInfo,
|
||||
ILoginResult,
|
||||
IMeResult,
|
||||
IMenuItem,
|
||||
ITableProto,
|
||||
ITableCrudProto,
|
||||
IUser,
|
||||
IRole,
|
||||
IOperationLog,
|
||||
WithId,
|
||||
IShrimpOperator,
|
||||
IShrimpPowerData,
|
||||
ShrimpRequestFn,
|
||||
IShrimpClient,
|
||||
ShrimpFieldType,
|
||||
FieldEditable,
|
||||
IFieldTypeHandler,
|
||||
IFieldConfig,
|
||||
IFieldRule,
|
||||
IPageQuery,
|
||||
IPageResult,
|
||||
IShrimpCondition,
|
||||
IShrimpPopulate,
|
||||
IShrimpQuery,
|
||||
IPowerItem,
|
||||
IUserProto,
|
||||
IRoleProto,
|
||||
} from "./IShrimpType"
|
||||
|
||||
// ===== 框架核心 =====
|
||||
export {
|
||||
Shrimp,
|
||||
ShrimpModule,
|
||||
ShrimpTableModule,
|
||||
ShrimpPower,
|
||||
RootShrimpPower,
|
||||
UserModule,
|
||||
RoleModule,
|
||||
defaultTypeHandlers,
|
||||
} from "./Shrimp"
|
||||
|
||||
export type {
|
||||
IShrimpModuleOptions,
|
||||
ShrimpCrud,
|
||||
} from "./Shrimp"
|
||||
|
||||
// ===== 数据源(纯内存,无外部依赖) =====
|
||||
export { MemoryShrimpCrud } from "./MemoryShrimpCrud"
|
||||
|
||||
// ===== 可选模块(纯框架代码,无外部依赖) =====
|
||||
export { OperationLogModule } from "./OperationLogModule"
|
||||
+7
-75
@@ -1,78 +1,10 @@
|
||||
/**
|
||||
* Shrimp Server — 公共 API
|
||||
* 默认入口 = 核心 (0 重依赖)
|
||||
*
|
||||
* 内部实现细节(如 ShrimpEvent)未在此处导出,业务请只依赖本文件再导出的符号。
|
||||
* - import ... from 'shrimp-server' => 这个文件 / src/core.ts(纯框架,无 peer)
|
||||
* - import ... from 'shrimp-server/server' => src/server.ts(再带 ShrimpServer + Mongo)
|
||||
*
|
||||
* 该文件存在的意义是兼容不识别 `package.json#exports` 的旧工具链(npm < 7、
|
||||
* 部分 bundler 的 legacy resolve),让 `main: src/index.ts` 也能正常工作。
|
||||
*/
|
||||
|
||||
// ===== 类型 / 接口 / 枚举 =====
|
||||
export {
|
||||
ShrimpCode,
|
||||
ShrimpError,
|
||||
ShrimpFieldTypes,
|
||||
ShrimpConfig,
|
||||
isShrimpCondition,
|
||||
} from "./IShrimpType"
|
||||
|
||||
export type {
|
||||
ILabel,
|
||||
IShrimpResponse,
|
||||
IModuleInfo,
|
||||
ILoginResult,
|
||||
IMeResult,
|
||||
IMenuItem,
|
||||
ITableProto,
|
||||
ITableCrudProto,
|
||||
IUser,
|
||||
IRole,
|
||||
IOperationLog,
|
||||
WithId,
|
||||
IShrimpOperator,
|
||||
IShrimpPowerData,
|
||||
ShrimpRequestFn,
|
||||
IShrimpClient,
|
||||
ShrimpFieldType,
|
||||
FieldEditable,
|
||||
IFieldTypeHandler,
|
||||
IFieldConfig,
|
||||
IFieldRule,
|
||||
IPageQuery,
|
||||
IPageResult,
|
||||
IShrimpCondition,
|
||||
IShrimpPopulate,
|
||||
IShrimpQuery,
|
||||
IPowerItem,
|
||||
IUserProto,
|
||||
IRoleProto,
|
||||
} from "./IShrimpType"
|
||||
|
||||
// ===== 框架核心 =====
|
||||
export {
|
||||
Shrimp,
|
||||
ShrimpModule,
|
||||
ShrimpTableModule,
|
||||
ShrimpPower,
|
||||
RootShrimpPower,
|
||||
UserModule,
|
||||
RoleModule,
|
||||
defaultTypeHandlers,
|
||||
} from "./Shrimp"
|
||||
|
||||
export type {
|
||||
IShrimpModuleOptions,
|
||||
ShrimpCrud,
|
||||
} from "./Shrimp"
|
||||
|
||||
// ===== 服务端实现(Express + Mongoose + JWT) =====
|
||||
export {
|
||||
ShrimpServer,
|
||||
UserSchema,
|
||||
RoleSchema,
|
||||
OperationLogSchema,
|
||||
} from "./ShrimpServer"
|
||||
|
||||
// ===== 数据源 =====
|
||||
export { MongoShrimpCrud } from "./MongoShrimpCrud"
|
||||
export { MemoryShrimpCrud } from "./MemoryShrimpCrud"
|
||||
|
||||
// ===== 可选模块 =====
|
||||
export { OperationLogModule } from "./OperationLogModule"
|
||||
export * from "./core"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Shrimp 服务端入口 — 包含 Express + Mongoose + JWT 实现
|
||||
*
|
||||
* 在 core 之上额外暴露:
|
||||
* - ShrimpServer:Express Router + JWT 鉴权 + Mongo 连接
|
||||
* - MongoShrimpCrud:Mongoose 数据源
|
||||
* - 内置 User/Role/OperationLog 的 Mongoose Schema
|
||||
*
|
||||
* 这一入口需要消费方安装 peer 依赖:express / mongoose / jsonwebtoken / bcrypt。
|
||||
* 不需要这些时请改从 'shrimp-server' 导入,不会触发任何 peer。
|
||||
*/
|
||||
|
||||
// 再导出所有 core 的符号,'shrimp-server/server' 单一入口就够用
|
||||
export * from "./core"
|
||||
|
||||
// ===== 服务端实现(Express + Mongoose + JWT) =====
|
||||
export {
|
||||
ShrimpServer,
|
||||
UserSchema,
|
||||
RoleSchema,
|
||||
OperationLogSchema,
|
||||
} from "./ShrimpServer"
|
||||
|
||||
// ===== Mongoose 数据源 =====
|
||||
export { MongoShrimpCrud } from "./MongoShrimpCrud"
|
||||
@@ -109,17 +109,25 @@ describe("MemoryShrimpCrud", () => {
|
||||
expect(result.total).toBe(0)
|
||||
})
|
||||
|
||||
it("populateItems 旁挂 _label", async () => {
|
||||
it("populateItems 旁挂 _label (通过 crudRegistry 解析)", async () => {
|
||||
const tagCrud = new MemoryShrimpCrud<{ _id: string; name: string }>()
|
||||
await tagCrud.add({ name: "热门" } as any)
|
||||
await tagCrud.add({ name: "促销" } as any)
|
||||
|
||||
const itemCrud = new MemoryShrimpCrud<IItem>()
|
||||
itemCrud.getRelatedCrud = (key) => key === "tag" ? tagCrud as any : undefined
|
||||
const a = await itemCrud.add({ name: "苹果", price: 5, tags: ["1", "2"] } as any)
|
||||
|
||||
const registry = new Map<string, any>([["tag", tagCrud]])
|
||||
const items = [{ ...a }] as any[]
|
||||
await itemCrud.populateItems(items, [{ moduleKey: "tag", key: "tags", labelKey: "name" }])
|
||||
await itemCrud.populateItems(items, [{ moduleKey: "tag", key: "tags", labelKey: "name" }], registry)
|
||||
expect(items[0].tags_label).toEqual(["热门", "促销"])
|
||||
})
|
||||
|
||||
it("populateItems 不传 crudRegistry 静默跳过(不抛错,不旁挂)", async () => {
|
||||
const itemCrud = new MemoryShrimpCrud<IItem>()
|
||||
const a = await itemCrud.add({ name: "苹果", price: 5, tags: ["1"] } as any)
|
||||
const items = [{ ...a }] as any[]
|
||||
await itemCrud.populateItems(items, [{ moduleKey: "tag", key: "tags", labelKey: "name" }])
|
||||
expect(items[0].tags_label).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { Shrimp, ShrimpTableModule, MemoryShrimpCrud, ShrimpFieldTypes } from "../src"
|
||||
import type { IFieldConfig } from "../src"
|
||||
|
||||
describe("crudRegistry 自动登记", () => {
|
||||
it("ShrimpTableModule 构造时自动把 dataSource 登记到 root.crudRegistry", () => {
|
||||
const shrimp = new Shrimp()
|
||||
const crud = new MemoryShrimpCrud()
|
||||
new ShrimpTableModule(shrimp, "tag", crud, [], "标签")
|
||||
expect(shrimp.crudRegistry.get("tag")).toBe(crud)
|
||||
})
|
||||
|
||||
it("通过 ShrimpTableModule.list 走 relation populate,只需声明字段无需手动 wire", async () => {
|
||||
const shrimp = new Shrimp()
|
||||
|
||||
const tagCrud = new MemoryShrimpCrud<{ _id: string; name: string }>()
|
||||
new ShrimpTableModule(shrimp, "tag", tagCrud, [
|
||||
{ key: "name", label: "名称", type: ShrimpFieldTypes.String },
|
||||
], "标签").initCrud()
|
||||
const t1 = await tagCrud.add({ name: "热门" } as any)
|
||||
const t2 = await tagCrud.add({ name: "促销" } as any)
|
||||
|
||||
const productCrud = new MemoryShrimpCrud<{ _id: string; code: string; tagIds: string[] }>()
|
||||
const productFields: IFieldConfig[] = [
|
||||
{ key: "code", label: "编码", type: ShrimpFieldTypes.String },
|
||||
{
|
||||
key: "tagIds", label: "标签", type: ShrimpFieldTypes.Relation,
|
||||
relation: { moduleKey: "tag", labelKey: "name" }, multiple: true,
|
||||
},
|
||||
]
|
||||
const productModule = new ShrimpTableModule(shrimp, "product", productCrud, productFields, "商品")
|
||||
productModule.initCrud()
|
||||
|
||||
await productCrud.add({ code: "P001", tagIds: [t1._id, t2._id] } as any)
|
||||
|
||||
const result = await productModule.list({})
|
||||
expect((result.list[0] as any).tagIds_label).toEqual(["热门", "促销"])
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -16,5 +16,5 @@
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*", "test/**/*", "examples/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "examples/**/web/**"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user