Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c99ea0e18c | ||
|
|
652b31052e |
@@ -96,6 +96,33 @@ import type { IFieldConfig, IMenuItem, IMeResult } from "shrimp-server/types"
|
||||
|
||||
## 约定
|
||||
|
||||
shrimp 不是铁板一块的协议,它是**一组可选拼装件**。每个件对外有自己的契约,你用了哪个件就被哪个件的契约绑住,不用就不必遵守。盘点时按**作用域**分两层。
|
||||
|
||||
### 🌐 全局约定(只要你说"我在写 shrimp",就跑不掉)
|
||||
|
||||
| 契约 | 说明 |
|
||||
|---|---|
|
||||
| `IModuleInfo` 形状 | 任何模块声明都得这么写 — 见 [src/IShrimpType.ts](src/IShrimpType.ts) `IModuleInfo` |
|
||||
| `IFieldConfig` 形状 | 字段就这样描述(`name` / `label` / `type` / `relation` / `rules` …) |
|
||||
| Shrimp 树结构 | `parent`/`children` 嵌套、`getFullPath()` 路径语义 |
|
||||
| Power / Role 声明 | `power` 树形 + `role` 持 `powers[]` |
|
||||
| `ILabel` 多语言形态 | 见下方 §国际化 — 6 选 1 的标签类型 |
|
||||
|
||||
### 🔌 条件约定(用了某个件,才被它的契约绑住)
|
||||
|
||||
| 启用前提 | 它的契约 |
|
||||
|---|---|
|
||||
| **使用 `ShrimpTable`(前端表格组件)** | • list 响应必须是 `IPageResult` = `{ list, total, page, pageSize, totalPages }`<br>• 字段 `type` 必须落在内置枚举:`string` / `number` / `boolean` / `date` / `select` / `textarea` / `password` / `relation` / `json` / `tab` —— 或自行注册 `IFieldTypeHandler`<br>• populate 字段旁挂 `${key}_label`(数组关联挂数组) |
|
||||
| **使用 `buildShrimpRouter` / `ShrimpServer`** | • HTTP 路由形状:`GET /config`、`POST /login`、`GET /me`、`POST /<modulePath>/<event>`<br>• JWT 携带:`Authorization: Bearer <token>`<br>• 后端需实现 `IUserAccessor`(getJwtPayload / isUserActive / getUserRoleIds / isRoleSuperAdmin / getRolePowers / getUserDisplayName) — 见 [src/forExpress/types.ts](src/forExpress/types.ts) |
|
||||
| **使用 `MongoShrimp`** | • User/Role/OperationLog 的 mongoose Schema 形状<br>• `onDbConnected` 钩子时序:连接就绪后才能注册需要 DB 的模块 |
|
||||
| **挂 `OperationLogModule`** | • CRUD 动作自动写日志的字段集(operator / module / action / target / time) |
|
||||
| **使用默认 `Field*.vue` 渲染**(前端) | • 字段 `type` 必须是内置枚举之一,否则需注册 `typeHandler` 才能渲染 |
|
||||
| **使用 Shrimp 子模块机制** | • 子表 `ownerKey` 是后端注册侧独占,**不进 fields[]**<br>• 前端只发通用 `ownerId`,不感知 `ownerKey` 的实际字段名 |
|
||||
|
||||
> 替换某个件,这个件的条件契约自动失效。例如:你换掉 `ShrimpTable` 用 AG-Grid,list 响应格式和字段 type 枚举两条对你都不再生效;你不挂 `OperationLogModule`,日志契约就不存在。
|
||||
|
||||
---
|
||||
|
||||
### 国际化 (ILabel)
|
||||
|
||||
所有前端可见文本字段(`label` / `tip` / `menu.label` / `power.label` / `field.label` / `rule.message` …)统一类型 `ILabel`。**用哪种形态取决于你写在哪一层**。
|
||||
|
||||
+1
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shrimp-server",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.0",
|
||||
"private": true,
|
||||
"description": "Shrimp 框架后端 — 声明式字段驱动的 Node.js 管理后台框架(Express + Mongoose + JWT)",
|
||||
"main": "src/main/index.ts",
|
||||
@@ -10,14 +10,6 @@
|
||||
"types": "./src/main/index.ts",
|
||||
"default": "./src/main/index.ts"
|
||||
},
|
||||
"./expmon": {
|
||||
"types": "./src/main/expmon.ts",
|
||||
"default": "./src/main/expmon.ts"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./src/main/types.ts",
|
||||
"default": "./src/main/types.ts"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
|
||||
+8
-2
@@ -1,6 +1,12 @@
|
||||
/**
|
||||
* Shrimp 前后端共用类型定义
|
||||
* 定义错误码、协议、通用数据格式
|
||||
* Shrimp 类型契约 — 前后端 wire protocol 定义
|
||||
*
|
||||
* @contractVersion 1.0.0
|
||||
*
|
||||
* 改动此文件需要:
|
||||
* 1. 升 @contractVersion(语义化版本)
|
||||
* 2. 同步到 shrimp-admin/src/IShrimpType.ts,更新其 @syncedFrom
|
||||
* 3. 通知消费侧对齐
|
||||
*/
|
||||
|
||||
/** 业务错误码 */
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Shrimp expmon 入口 — Express + Mongoose 整合方案
|
||||
*
|
||||
* 在 core 之上把 forMongo(MongoShrimp + 数据源)和 forExpress(buildShrimpRouter
|
||||
* + JWT)拼起来,导出开箱即用的 ShrimpServer 装配体。
|
||||
*
|
||||
* 这一入口需要消费方安装 peer:express / mongoose / jsonwebtoken / bcrypt。
|
||||
* 不需要时改从 'shrimp-server' 导入,不会触发任何 peer。
|
||||
*
|
||||
* 想换掉其中一块的高级用法:
|
||||
* - 自己造 ITokenAuth(看 createJwtTokenAuth 实现)
|
||||
* - 自己造 dataSource(实现 ShrimpCrud 接口)
|
||||
* - 直接 buildShrimpRouter(deps) 拼自己的 Express app,绕开本类
|
||||
*/
|
||||
|
||||
import { Router } from "express"
|
||||
import { MongoShrimp, UserSchema, RoleSchema, OperationLogSchema } from "../forMongo/MongoShrimp"
|
||||
import { buildShrimpRouter } from "../forExpress/buildShrimpRouter"
|
||||
import { createJwtTokenAuth } from "../forExpress/jwtAuth"
|
||||
import type { ITokenAuth, IUserAccessor } from "../forExpress/types"
|
||||
import type { RootShrimpPower } from "../Shrimp/ShrimpPower"
|
||||
import type { IUser, IRole, WithId } from "../IShrimpType"
|
||||
|
||||
// 再导出所有 core 的符号,'shrimp-server/expmon' 单一入口就够用
|
||||
export * from "./index"
|
||||
|
||||
// ===== forMongo / forExpress 公开符号 =====
|
||||
export { MongoShrimp, UserSchema, RoleSchema, OperationLogSchema } from "../forMongo/MongoShrimp"
|
||||
export { MongoShrimpCrud } from "../forMongo/MongoShrimpCrud"
|
||||
export { buildShrimpRouter } from "../forExpress/buildShrimpRouter"
|
||||
export { createJwtTokenAuth } from "../forExpress/jwtAuth"
|
||||
export type { IJwtTokenAuthOptions } from "../forExpress/jwtAuth"
|
||||
export type { ITokenAuth, IUserAccessor, IShrimpRouterDeps } from "../forExpress/types"
|
||||
|
||||
// ============== ShrimpServer ==============
|
||||
|
||||
export class ShrimpServer extends MongoShrimp implements IUserAccessor {
|
||||
router: Router
|
||||
jwtSecret: string
|
||||
config: Record<string, any>
|
||||
globalOptions: Record<string, any> = {}
|
||||
forcedOptionAccess = new Map<string, RootShrimpPower>()
|
||||
|
||||
constructor(jwtSecret: string, config: Record<string, any> = {}, mongodbUri: string = "mongodb://localhost:27017/shrimp") {
|
||||
super(mongodbUri)
|
||||
this.jwtSecret = jwtSecret
|
||||
this.config = config
|
||||
|
||||
this.router = buildShrimpRouter({
|
||||
shrimp: this,
|
||||
userModule: this.userModule,
|
||||
roleModule: this.roleModule,
|
||||
operationLogModule: this.operationLogModule,
|
||||
tokenAuth: this.createTokenAuth(),
|
||||
config: this.config,
|
||||
globalOptions: this.globalOptions,
|
||||
forcedOptionAccess: this.forcedOptionAccess,
|
||||
userAccessor: this,
|
||||
})
|
||||
}
|
||||
|
||||
registerOptions(key: string, options: any, access?: RootShrimpPower) {
|
||||
this.globalOptions[key] = options
|
||||
if (access) {
|
||||
this.forcedOptionAccess.set(key, access)
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建 Token 鉴权,默认 JWT。子类可覆盖换成 session 之类。 */
|
||||
protected createTokenAuth(): ITokenAuth {
|
||||
return createJwtTokenAuth(this.jwtSecret)
|
||||
}
|
||||
|
||||
// ============== 可覆盖的用户/角色字段访问钩子(实现 IUserAccessor) ==============
|
||||
|
||||
getJwtPayload(user: WithId<IUser>): Record<string, any> {
|
||||
return { _id: user._id, roleId: user.roleId }
|
||||
}
|
||||
|
||||
isUserActive(user: WithId<IUser>): boolean {
|
||||
return user.active === true
|
||||
}
|
||||
|
||||
getUserRoleIds(user: WithId<IUser>): string[] {
|
||||
return Array.isArray(user.roleId) ? user.roleId.map(String) : user.roleId ? [String(user.roleId)] : []
|
||||
}
|
||||
|
||||
isRoleSuperAdmin(role: WithId<IRole> | null): boolean {
|
||||
return !!role?.isSuperAdmin
|
||||
}
|
||||
|
||||
getRolePowers(role: WithId<IRole> | null): string[] {
|
||||
return role?.powers || []
|
||||
}
|
||||
|
||||
getUserDisplayName(user: WithId<IUser>): string {
|
||||
return user.nickname || ""
|
||||
}
|
||||
}
|
||||
+95
-12
@@ -1,19 +1,28 @@
|
||||
/**
|
||||
* Shrimp 核心入口 — 0 重依赖
|
||||
* Shrimp 单一入口 — 完整后端栈
|
||||
*
|
||||
* 只暴露 Shrimp 框架本体 + 内存数据源 + 类型定义,不引入
|
||||
* express / mongoose / jsonwebtoken。bcrypt 是 lazy import,
|
||||
* 不走 Password 字段就不会触发。
|
||||
* 包含框架核心 + 类型契约 + Express + Mongoose + JWT 装配体。
|
||||
*
|
||||
* 适用场景:
|
||||
* - 只想用 Shrimp 的 RPC dispatch + 模块/字段声明 + 内存 CRUD
|
||||
* - 自己接 HTTP / 自己写存储层
|
||||
* - 跑测试 / 跑无 DB 的 demo
|
||||
* 安装 peer:express / mongoose / jsonwebtoken / bcrypt。
|
||||
*
|
||||
* 需要完整后台(Express + Mongoose + JWT)请从 'shrimp-server/expmon' 导入。
|
||||
* 前端只想要类型契约,**不要**从这里 import — 见 shrimp-admin 的
|
||||
* IShrimpType.ts(契约副本),避免把 mongoose/express 牵进前端 bundle。
|
||||
*
|
||||
* 想换掉其中一块的高级用法:
|
||||
* - 自己造 ITokenAuth(看 createJwtTokenAuth 实现)
|
||||
* - 自己造 dataSource(实现 ShrimpCrud 接口)
|
||||
* - 直接 buildShrimpRouter(deps) 拼自己的 Express app,绕开 ShrimpServer
|
||||
*/
|
||||
|
||||
// ===== 类型 / 接口 / 枚举 =====
|
||||
import { Router } from "express"
|
||||
import { MongoShrimp } from "../forMongo/MongoShrimp"
|
||||
import { buildShrimpRouter } from "../forExpress/buildShrimpRouter"
|
||||
import { createJwtTokenAuth } from "../forExpress/jwtAuth"
|
||||
import type { ITokenAuth, IUserAccessor } from "../forExpress/types"
|
||||
import type { RootShrimpPower } from "../Shrimp/ShrimpPower"
|
||||
import type { IUser, IRole, WithId } from "../IShrimpType"
|
||||
|
||||
// ===== 类型契约 / 枚举 =====
|
||||
export {
|
||||
ShrimpCode,
|
||||
ShrimpError,
|
||||
@@ -64,9 +73,83 @@ export type { ShrimpCrud } from "../Shrimp/ShrimpCrud"
|
||||
export { UserModule, RoleModule } from "../Shrimp/UserRoleModule"
|
||||
export { OperationLogModule } from "../Shrimp/OperationLogModule"
|
||||
|
||||
// ===== 密码哈希(可注入,默认 bcrypt lazy import) =====
|
||||
// ===== 密码哈希 =====
|
||||
export { bcryptHasher, plainHasher } from "../PasswordHasher"
|
||||
export type { IPasswordHasher } from "../PasswordHasher"
|
||||
|
||||
// ===== 数据源(纯内存,无外部依赖) =====
|
||||
// ===== 数据源 =====
|
||||
export { MemoryShrimpCrud } from "../MemoryShrimpCrud"
|
||||
export { MongoShrimp, UserSchema, RoleSchema, OperationLogSchema } from "../forMongo/MongoShrimp"
|
||||
export { MongoShrimpCrud } from "../forMongo/MongoShrimpCrud"
|
||||
|
||||
// ===== Express 装配 =====
|
||||
export { buildShrimpRouter } from "../forExpress/buildShrimpRouter"
|
||||
export { createJwtTokenAuth } from "../forExpress/jwtAuth"
|
||||
export type { IJwtTokenAuthOptions } from "../forExpress/jwtAuth"
|
||||
export type { ITokenAuth, IUserAccessor, IShrimpRouterDeps } from "../forExpress/types"
|
||||
|
||||
// ============== ShrimpServer ==============
|
||||
|
||||
export class ShrimpServer extends MongoShrimp implements IUserAccessor {
|
||||
router: Router
|
||||
jwtSecret: string
|
||||
config: Record<string, any>
|
||||
globalOptions: Record<string, any> = {}
|
||||
forcedOptionAccess = new Map<string, RootShrimpPower>()
|
||||
|
||||
constructor(jwtSecret: string, config: Record<string, any> = {}, mongodbUri: string = "mongodb://localhost:27017/shrimp") {
|
||||
super(mongodbUri)
|
||||
this.jwtSecret = jwtSecret
|
||||
this.config = config
|
||||
|
||||
this.router = buildShrimpRouter({
|
||||
shrimp: this,
|
||||
userModule: this.userModule,
|
||||
roleModule: this.roleModule,
|
||||
operationLogModule: this.operationLogModule,
|
||||
tokenAuth: this.createTokenAuth(),
|
||||
config: this.config,
|
||||
globalOptions: this.globalOptions,
|
||||
forcedOptionAccess: this.forcedOptionAccess,
|
||||
userAccessor: this,
|
||||
})
|
||||
}
|
||||
|
||||
registerOptions(key: string, options: any, access?: RootShrimpPower) {
|
||||
this.globalOptions[key] = options
|
||||
if (access) {
|
||||
this.forcedOptionAccess.set(key, access)
|
||||
}
|
||||
}
|
||||
|
||||
/** 创建 Token 鉴权,默认 JWT。子类可覆盖换成 session 之类。 */
|
||||
protected createTokenAuth(): ITokenAuth {
|
||||
return createJwtTokenAuth(this.jwtSecret)
|
||||
}
|
||||
|
||||
// ============== 可覆盖的用户/角色字段访问钩子(实现 IUserAccessor) ==============
|
||||
|
||||
getJwtPayload(user: WithId<IUser>): Record<string, any> {
|
||||
return { _id: user._id, roleId: user.roleId }
|
||||
}
|
||||
|
||||
isUserActive(user: WithId<IUser>): boolean {
|
||||
return user.active === true
|
||||
}
|
||||
|
||||
getUserRoleIds(user: WithId<IUser>): string[] {
|
||||
return Array.isArray(user.roleId) ? user.roleId.map(String) : user.roleId ? [String(user.roleId)] : []
|
||||
}
|
||||
|
||||
isRoleSuperAdmin(role: WithId<IRole> | null): boolean {
|
||||
return !!role?.isSuperAdmin
|
||||
}
|
||||
|
||||
getRolePowers(role: WithId<IRole> | null): string[] {
|
||||
return role?.powers || []
|
||||
}
|
||||
|
||||
getUserDisplayName(user: WithId<IUser>): string {
|
||||
return user.nickname || ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Shrimp 纯类型入口
|
||||
*
|
||||
* 只导出 IShrimpType.ts 的全部协议(类型 + ShrimpCode enum + ShrimpError class),
|
||||
* 前端 shrimp-admin / 任何只需要协议定义的消费方从这里 import,
|
||||
* 不会触发任何运行时依赖。
|
||||
*/
|
||||
|
||||
export * from "../IShrimpType"
|
||||
Reference in New Issue
Block a user