@scenemesh/entity-engine
v0.2.16
Published
一个“元数据驱动 + 组件适配 + 动态关系 + 视图管线”式的实体引擎。以 **Model + View + FieldType + SuiteAdapter + DataSource** 为五大支点,统一 CRUD / 查询 / 引用管理 / 视图渲染 / 扩展注册,支持在运行期无侵入拼装出 **表单、网格、主从、看板、仪表盘、流程/树形视图** 等多形态界面。
Readme
@scenemesh/entity-engine
一个“元数据驱动 + 组件适配 + 动态关系 + 视图管线”式的实体引擎。以 Model + View + FieldType + SuiteAdapter + DataSource 为五大支点,统一 CRUD / 查询 / 引用管理 / 视图渲染 / 扩展注册,支持在运行期无侵入拼装出 表单、网格、主从、看板、仪表盘、流程/树形视图 等多形态界面。
设计原则:配置驱动、运行补全、插槽扩展、最小绑定、可模块化注入。 适合构建中后台、数据工作台、领域建模平台、低代码/AI 辅助生成系统。
目录一览
- 特性速览
- 与 workbench 集成全景示例(真实用法)
- 安装与 Peer 依赖
- 快速上手(客户端 / 服务端 / 混合渲染)
- 核心概念与架构示意
- 模型 (Model) 与视图 (View) 元数据
- 视图渲染与插槽扩展 (Renderers & Slots)
- 组件套件 (Suite Adapter) 与自定义 Widget
- 数据源与引用关系(References / Trees / Grouping)
- 查询 DSL(条件表达表达式结构)
- 服务端接入(Next.js Route Handler / tRPC 端点)
- 多租户架构(PostgreSQL Schema 隔离)
- 模块化扩展(动态加载 & AI 模块示例)
- 权限 / 导航 / 会话集成
- 调试与开发辅助(View Inspector / Studio Launcher)
- Roadmap
- FAQ
- License
1. 特性速览
| 能力 | 说明 |
| ---- | ---- |
| 元数据驱动 | 使用 IEntityModel + IEntityView 描述领域与 UI 形态,减少硬编码。 |
| 运行时补全 | 视图缺省字段、Widget、顺序等由 FieldTyper 与模型自动推导。 |
| 插槽扩展 | 通过命名渲染器 (Named Renderer) 在壳层 / 工具栏 / 行内插入自定义区域。 |
| 多视图内置 | form / grid / master-detail / shell / kanban / dashboard。 |
| 引用关系统一 | 基于引用表抽象支持一对多、多对多、树、反向查询、计数。 |
| 多对多编辑 | 内置 ReferenceEditMMComp 管理选择/删除/批量操作。 |
| 查询 DSL | 嵌套 AND/OR/NOT、Between/In/Contains 等操作符统一结构表达。 |
| 数据源抽象 | IEntityDataSource 屏蔽实现,默认 tRPC + Prisma(可扩展 REST / GraphQL)。 |
| 模块化 | 运行期动态加载模块(esm.sh / 本地),扩展模型、组件、动作、AI 工具。 |
| 组件套件 | UI 套件适配器(Mantine / 自定义 / additions suite),解耦 UI 风格。 |
| 行为注册 | Action / Event / Servlet 三类行为管线统一注册与调用。 |
| DevTools | View Inspector / Studio Launcher 提供运行态调试与可视化。 |
| 类型安全 | TypeScript + zod(字段 schema 可选)保障运行与编译期安全。 |
目标:用最少的“约束 + 元信息”表达,驱动出尽可能多的动态 UI 与行为。 - 新增视图实现 - 自定义 Widget - 命名 Renderer (Slot) - 字段类型扩展 (Field Type Extension) - 数据源扩展 (DataSource Extension) - 校验与默认值 (Validation & Defaults) - tRPC 集成 (tRPC Integration) - 常用接口参考 (API Reference Snapshot) - IEntityModel - IEntityField - IEntityView - IEntityDataSource (节选) - Roadmap - 贡献指南 (Contributing) - 许可 (License) - 常见问题 (FAQ) - 致谢 (Acknowledgements)
2. 与 workbench 集成全景示例
apps/workbench 展示了一个真实集成:
客户端 Provider 封装示例(简化自 entity-engine-provider-warpper.tsx):
// src/entity/provider/entity-engine-provider-warpper.tsx
'use client';
import { useRouter } from 'next/navigation';
import { createEntityEngineProvider, useEntityEngine, EntityViewInspector } from '@scenemesh/entity-engine';
import { AdditionsSuiteAdapter } from '@scenemesh/entity-suite-additions';
import { EntityAIModule } from '@scenemesh/entity-engine-aimodule';
import { EntityEngineStudioLauncher } from '@scenemesh/entity-engine-studio';
export function EntityEngineProviderWrapper({ children }: { children: React.ReactNode }) {
const router = useRouter();
const EntityEngineProvider = createEntityEngineProvider({
// config: { models, views }, // 可按需注入模型与视图
suiteAdapters: [new AdditionsSuiteAdapter()],
suiteAdapter: { suiteName: 'additions', suiteVersion: '1.0.0' },
router: { navigate: (path, state) => router.push(path) },
permissionGuard: { checkPermission: async () => true },
renderers: [
{ ...EntityViewInspector },
{ ...EntityEngineStudioLauncher },
{ name: 'view-tool-2', slotName: 'view-tool', renderer: (ctx) => <button>工具扩展</button> },
],
settings: {
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8082',
endpoint: process.env.NEXT_PUBLIC_API_ENDPOINT || '/api/ee',
authenticationEnabled: true,
},
modules: [new EntityAIModule()],
});
return <EntityEngineProvider>{children}</EntityEngineProvider>;
}服务端路由集成(Next.js App Router):
// app/api/ee/[[...slug]]/route.ts
import { EnginePrimitiveInitializer, fetchEntityEntranceHandler } from '@scenemesh/entity-engine/server';
import { EntityAIModule } from '@scenemesh/entity-engine-aimodule';
import { models, views } from 'src/entity/model-config';
const init = new EnginePrimitiveInitializer({ models, views, modules: [new EntityAIModule()] });
const handler = (req: Request) => fetchEntityEntranceHandler({ request: req, endpoint: '/api/ee', initializer: init });
export { handler as GET, handler as POST };在界面中使用容器组件(动态视图渲染):
import { EntityViewContainer } from '@scenemesh/entity-engine';
export function ProductGrid() {
return (
<EntityViewContainer
modelName="product"
viewType="grid"
viewName="productGridView"
maintain={{ pageSize: 20 }}
/>
);
}树 + 引用 + 流程可视化结合(节选自场景设计器):
const ds = engine.datasourceFactory.getDataSource();
// 基于引用关系查找根节点
await ds.findMany({
modelName: 'scene',
query: {
pageIndex: 1,
pageSize: 1,
references: {
fromModelName: 'product',
fromFieldName: 'rootScene',
fromObjectId: currentProductId,
toModelName: 'scene',
},
},
});
// 树结构展开
await ds.findTreeObjects({ modelName: 'scene', fieldName: 'children', rootObjectId });AI 模块集成:EntityAIModule 注入后可在渲染器/工具链中暴露 AI 辅助功能(例如智能填表、生成字段描述、查询建议等)。
更多完整示例可参考:
apps/workbench/src/entity与apps/workbench/src/viewports/scene。
3. 安装与 Peer 依赖
安装
# npm
npm i @scenemesh/entity-engine
# yarn
yarn add @scenemesh/entity-engine
# pnpm
pnpm add @scenemesh/entity-engine必需 peer 依赖
- react >=18 <20
- react-dom >=18 <20
- react-hook-form >=7 <8
- Mantine(供内置视图与检查器使用)
- @mantine/core 8.2.5
- @mantine/hooks 8.2.5
- @mantine/modals 8.2.5
- @mantine/notifications 8.2.5
- mantine-datatable 8.2.0
可选 peer(仅使用 server 能力时)
- @prisma/client(与 prisma CLI)
样式引入(必需)
import '@scenemesh/entity-engine/main.css'4. 快速上手
假设你在一个包含本包的 monorepo(已安装依赖)中开发。
Next.js(App Router)最小用法:
// app/layout.tsx
import '@scenemesh/entity-engine/main.css'
import { EntityEngineProvider } from 'src/entity-provider' // 参考下文示例或自行封装 Provider
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<EntityEngineProvider>{children}</EntityEngineProvider>
</body>
</html>
)
}Vite(或 CRA)最小用法:
// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import '@scenemesh/entity-engine/main.css'
import { EngineInitializer, getEntityEngine } from '@scenemesh/entity-engine'
async function bootstrap() {
const initializer = new EngineInitializer([], [])
await getEntityEngine(initializer)
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<div>Entity Engine Ready</div>
</React.StrictMode>
)
}
bootstrap()提示:本包将 React/ReactDOM、部分 UI 库设为 peer 依赖;构建产物 external 常见 UI 库,避免与你的应用重复打包或版本冲突。
构建 / 开发
# 构建产物 (dist)
yarn build
# 持续编译 (tsc --watch)
yarn dev
# 数据库(生成 Prisma Client / 迁移)
yarn db:generate # prisma migrate dev
yarn db:migrate # prisma migrate deploy
yarn db:push # prisma db push
yarn db:studio # 启动数据浏览初始化引擎(最小)
import { EngineInitializer, getEntityEngine } from '@scenemesh/entity-engine';
// 1. 定义模型(最小示例)
const UserModel = {
name: 'user',
title: '用户',
fields: [
{ name: 'id', title: 'ID', type: 'string', isPrimaryKey: true },
{ name: 'name', title: '姓名', type: 'string', searchable: true, isRequired: true },
{ name: 'age', title: '年龄', type: 'number', searchable: true },
{ name: 'role', title: '角色', type: 'enum', typeOptions: { options: [ { label: '管理员', value: 'admin' }, { label: '访客', value: 'guest' } ] }, searchable: true },
],
} as const;
// 2. 定义视图(若不定义,也可由元数据自动补全创建默认视图)
const UserGridView = {
name: 'user-grid',
title: '用户列表',
modelName: 'user',
viewType: 'grid',
items: [
{ name: 'id', width: 120 },
{ name: 'name', flex: 1 },
{ name: 'age', width: 80 },
{ name: 'role', width: 120 },
],
};
// 3. 初始化(一次性调用)
async function bootstrap() {
const initializer = new EngineInitializer([UserModel], [UserGridView]);
const engine = await getEntityEngine(initializer);
console.log(engine.toString());
}
bootstrap();在 React 中使用某个视图(手动装配模式)
import React from 'react';
import { getEntityEngine } from '@scenemesh/entity-engine';
export function UserGridContainer() {
const [engine, setEngine] = React.useState<any>();
React.useEffect(() => {
getEntityEngine().then(setEngine); // 已初始化后无需再次传入 initializer
}, []);
if (!engine) return <div>Loading...</div>;
const view = engine.metaRegistry.findView('user', 'grid');
if (!view) return <div>View not found</div>;
const supplemented = view.toSupplementedView();
const gridView = engine.componentRegistry.getView('grid');
if (!gridView) return <div>Grid view component missing</div>;
const Comp = gridView.Component;
return <Comp model={engine.metaRegistry.getModel('user')!} viewData={supplemented} behavior={{ mode: 'display' }} />;
}5. 核心概念与架构
| 概念 | 接口 / 类 | 作用 |
| ---- | --------- | ---- |
| 实体字段 | IEntityField | 描述字段元信息(类型、校验、引用、搜索能力等)。 |
| 实体模型 | IEntityModel / EntityModelDelegate | 字段集合 + 行为补全(默认值 / Schema / QueryMeta)。 |
| 实体视图 | IEntityView / EntityViewDelegate | 描述一个模型的某种可视化(grid / form / master-detail / shell)。 |
| 视图字段项 | IEntityViewField | 与模型字段映射,可定义 widget、顺序、显示条件等。 |
| 引擎 | EntityEngine | 单例;聚合 metaRegistry / fieldTyperRegistry / dataSourceFactory / componentRegistry。 |
| 元数据注册表 | EntityMetaRegistry | 管理模型、视图、菜单;生成缺省视图。 |
| 组件注册表 | EntityComponentRegistry | 管理视图实现、Widget 套件、命名 Renderer。 |
| 组件套件适配器 | IEntityComponentSuiteAdapter | 一组 Widget 的命名空间(built-in 套件)。 |
| Widget | EntityWidget | 针对单字段或容器/引用呈现的可复用 UI 单元。 |
| 渲染器 | IEntityNamedRenderer | Slot 渲染扩展点(装饰 / 布局 / 行内渲染)。 |
| 字段类型 Typer | IModelFieldTyper | 决定默认值 / 默认 Widget / 查询能力。 |
| 数据源 | IEntityDataSource | CRUD + 引用 + 树形查询抽象。默认实现:tRPCEntityObjectDataSource。 |
| 初始化器 | EngineInitializer | 首次创建引擎时批量注册模型、视图、套件、渲染器。 |
架构概览
+---------------------------+
| EntityEngine | (Singleton)
+---------------------------+
| | | |
v v v v
+---------+ +-----------+ +--------+ +----------------+
| Meta | | FieldType | | Data | | Component |
| Registry| | Registry | |Source | | Registry |
+----+----+ +-----+-----+ +---+----+ +-------+--------+
| | | |
Models <->| | | |<-> Views Impl
Views <->| | | |<-> Widget Suites
| | | |<-> Renderers
| | | |
v v v v
Delegates FieldTypers TRPC DS React Components关键流:
- 初始化阶段:
EngineInitializer注册模型 / 视图 / 套件 / 渲染器。 - 运行时请求某视图:从
metaRegistry获取IEntityViewDelegate,补全 => 视图组件执行数据加载(通过datasourceFactory.getDataSource())。 - 用户操作(增删改) -> DataSource (tRPC) -> Server (Prisma) -> 结果回显。
6. 目录结构概览
简化摘录:
src/
core/
engine/ # 引擎与初始化
datasources/ # 数据源工厂 & tRPC 实现
delegate/ # Delegate 封装 (Model/View/Menu)
fieldtypes/ # 字段类型注册表
theme/ # 主题 (占位 / 可扩展)
types/ # 核心类型接口 (engine / delegate / datasource ...)
components/
views/ # 视图实现 (form / grid / mastail / shell)
types/ # 视图 & widget 类型定义
share/ # 公共共享组件 (search-panel 等)
build-ins/
suite/ # 内置组件套件 & widgets
services/api/trpc/ # tRPC 客户端工具
lib/data/ # 数据转换工具 (convertRawEntityObject 等)
types/ # 元数据 / 数据 / 字段 / 样式 类型
uikit/ # UI 基础 (provider / surface 等)7. 模型与视图元数据
模型(IEntityModel)定义领域结构,视图(IEntityView)定义展现形态。未定义的视图可按模型自动补全生成。大型项目建议将模型拆分多个文件并汇总导出。
示例:片段(来自 workbench model-config.ts)
export const models: IEntityModel[] = [
{
name: 'product',
title: '产品',
fields: [
{ name: 'name', title: '名称', type: 'string', isRequired: true, searchable: true },
{ name: 'price', title: '价格', type: 'number' },
{ name: 'rootScene', title: '根场景', type: 'one_to_one', refModel: 'scene' },
],
},
{
name: 'scene',
title: '场景',
fields: [
{ name: 'title', title: '标题', type: 'string', isRequired: true },
{ name: 'children', title: '子场景', type: 'one_to_many', refModel: 'scene' },
],
},
];视图定义示例
export const views: IEntityView[] = [
{
name: 'productGridView',
title: '产品列表',
modelName: 'product',
viewType: 'grid',
items: [ { name: 'name' }, { name: 'price' }, { name: 'rootScene' } ],
},
{
name: 'sceneFormView',
title: '场景表单',
modelName: 'scene',
viewType: 'form',
items: [ { name: 'title' }, { name: 'children', widget: 'reference-many' } ],
},
];8. 视图渲染与插槽扩展
通过注册命名渲染器(IEntityNamedRenderer)向视图生命周期插入 UI:
| slotName 示例 | 典型用途 |
| -------------- | -------- |
| view-tool | 表格/表单顶部工具条扩展 |
| shell-settings-item2 | 系统壳层设置菜单条目 |
| record-inline | 行内扩展按钮/标签 |
注册示例:
renderers: [
{ name: 'view-tool-2', slotName: 'view-tool', renderer: (ctx) => <span>工具扩展 {ctx.model.name}</span> },
{ ...EntityViewInspector }, // 内置调试器
]上层应用可通过 DSL + 权限系统动态控制哪些渲染器激活。
9. 组件套件 (Suite Adapter) 与自定义 Widget
组件套件用于将“抽象字段语义”映射为“具体 UI 实现”,不同 suiteAdapter 可复用同一组模型与视图元数据。
实现要点:
- 实现接口
IEntityComponentSuiteAdapter - 提供
suiteName、版本与getWidget(name) - 在 Provider 中通过
suiteAdapters注册,并指定当前使用的suiteAdapter
你可以同时注册多套适配器(例如 Mantine / Additions),再让用户在运行时切换主题或 UI 表现。
10. 数据源与引用 / 树 / 分组
数据源接口:IEntityDataSource,默认实现:TRPCEntityObjectDataSource。
常用方法:
| 方法 | 作用 |
| ---- | ---- |
| findMany | 分页查询实体对象 |
| create / update / delete | 基础 CRUD |
| findReferences / findReferencesCount | 引用关系列表与计数 |
| createReference / deleteReference / deleteReferences | 多对多关系增删 |
| findTreeObjects | 递归树结构(支持 children 字段) |
| findGroupedObjects | 根据字段或时间/范围聚合分组 |
多对多引用 UI 可直接使用内置组件:
import { ReferenceEditMMComp } from '@scenemesh/entity-engine';
// 在一个自定义表单 Widget 内:
<ReferenceEditMMComp
fieldType={{ /* 来自 view 字段解析 */ }}
object={currentObject}
value={selectedIds}
onChange={(ids) => setSelectedIds(ids)}
/>11. 查询 DSL
查询结构由 IEntityQuery + IEntityQueryItem 组成:
const query = {
pageIndex: 1,
pageSize: 20,
filter: {
and: [
{ field: 'name', operator: 'contains', value: 'x' },
{ or: [ { field: 'status', operator: 'eq', value: 'active' }, { field: 'status', operator: 'eq', value: 'draft' } ] },
],
},
};未来规划:提供“可视化 Query Builder + 表达式编译器”生成最终 Prisma where 条件。
11. 服务端接入(Next.js)
沿用 tRPC 风格:
- 创建
EnginePrimitiveInitializer注入模型与视图 - 在 Route Handler 中调用
fetchEntityEntranceHandler - 客户端 Provider 使用相同 endpoint
已支持的后端能力:对象 CRUD / 引用 / 树 / 分组 / 视图+模型增量下发。
12. 多租户架构(Multi-Tenancy Architecture)
基于 PostgreSQL Schema 隔离,每个租户拥有独立 schema,通过 PrismaService 管理连接池,实现数据强隔离与高性能并发访问。JWT token 自动提取 tenantId 并切换到对应 schema。
核心特性
| 特性 | 说明 |
| ---- | ---- |
| Schema 隔离 | 每个租户独立 PostgreSQL schema(tenant_<id>),原生数据隔离 |
| 连接池复用 | 自动管理租户 Prisma 客户端,按需创建与复用连接 |
| JWT 自动提取 | 从 Authorization header 解析 tenantId,无需手动传参 |
| 强制访问控制 | 数据 API 自动验证租户上下文,防止越权访问 |
| 灵活 ID 格式 | 支持 UUID(推荐)和 Base62 短 ID(10-12位) |
服务端使用
1. 直接使用 PrismaService(适用于自定义后端逻辑)
import { PrismaService } from '@scenemesh/entity-engine/server';
// 获取租户专用的 Prisma 客户端
const tenantId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // 从 JWT 提取
const prisma = await PrismaService.getTenantClient(tenantId);
// 操作租户数据(自动使用租户 schema)
const users = await prisma.entityObject.findMany({
where: { modelName: 'user' }
});
// 连接池管理
PrismaService.getActiveConnectionCount(); // 查看活跃连接数
await PrismaService.disconnectTenant(tenantId); // 断开指定租户连接2. tRPC 集成(已内置,开箱即用)
tRPC context 自动从 JWT token 提取 tenantId 并创建租户上下文:
// src/services/api/trpc.ts (已实现,无需修改)
export const createTRPCContext = async (opts, initializer) => {
const engine = await getEntityEnginePrimitive(initializer);
// 自动从 Authorization header 解析 JWT
const authHeader = opts.headers.get('authorization');
let tenantId: string | undefined;
let db: PrismaClient | undefined;
if (authHeader?.startsWith('Bearer ')) {
// 解析 JWT payload 获取 tenantId
const payload = parseJWT(token);
tenantId = payload.tenantId;
// 获取租户专用数据库客户端
if (tenantId && PrismaService.isValidTenantId(tenantId)) {
db = await PrismaService.getTenantClient(tenantId);
}
}
return { db, engine, tenantId };
};JWT Token Payload 结构示例:
{
"userId": "user_123",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"role": "admin",
"iat": 1735545600,
"exp": 1735632000
}客户端集成
客户端 tRPC 调用自动注入 Authorization header(从 localStorage.access_token 读取):
// src/services/api/trpc/react.tsx (已实现)
httpBatchLink({
url: getUrl('/trpc'),
headers: () => {
const headers = new Headers();
const token = localStorage.getItem('access_token');
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
}
})数据隔离保障
所有数据 API 内部自动执行租户检查:
// 数据 API(如 listObjects、createObject)强制要求租户上下文
function requireTenantDatabase(ctx: ApiContext): asserts ctx {
if (!ctx.db || !ctx.tenantId) {
throw new Error('Authentication required: Please login to access tenant data');
}
}
// 元数据 API(如 findPlainConfig)不需要租户隔离
export async function findPlainConfigLogic(ctx: ApiContext, input) {
// 仅访问模型/视图元数据,无需租户数据库
return ctx.engine.metaRegistry.getModel(input.modelName);
}最佳实践
- ✅ 适用场景:多租户 SaaS、企业级应用、数据隔离要求高的场景
- ⚠️ 注意事项:
- 租户 schema 需要独立执行 Prisma 迁移
- JWT token 必须可信(使用签名验证)
- 生产环境建议监控连接池大小与内存占用
更多细节(schema 迁移、性能优化、故障排查)请参见官方文档。
13. 模块化扩展 & AI 集成
模块(IEntityModule)可:
- 注册额外模型 / 视图
- 注册渲染器 / 动作 / Servlet
- 注入 AI 功能(见
EntityAIModule)
动态加载:客户端可通过 esm.sh/<package>/dist/index.js 按需拉取(需保证导出 default)。
14. 权限 / 导航 / 会话
Provider 选项:
| 选项 | 作用 |
| ---- | ---- |
| router.navigate(path,state) | 桥接到应用路由(Next.js push / React Router) |
| permissionGuard.checkPermission(action) | 统一权限校验入口 |
| settings | baseUrl / endpoint / authenticationEnabled |
| modules | 注入模块数组 |
你可在
checkPermission里访问全局用户上下文,实现模型/字段/操作级别控制。
15. 调试与开发工具
| 工具 | 描述 |
| ---- | ---- |
| EntityViewInspector | 在运行时查看当前视图的补全后结构(模型 / 字段 / widget 解析结果)。 |
| EntityEngineStudioLauncher | 打开交互式运行时面板,未来扩展模型编辑 / 视图热更新。 |
| 日志 | TRPCEntityObjectDataSource 在 URL 变更时输出新客户端创建日志。 |
16. Roadmap
- [ ] 查询 DSL -> 编译器 + 单元测试
- [ ] 模型 / 视图 版本化与快照导出
- [ ] 引用关系附加属性(排序 / 标签 / 权重)
- [ ] 渲染器 DevTools 面板
- [ ] 模块远程签名校验与缓存策略
- [ ] 多数据源聚合(federation)
- [ ] 表达式求值沙箱化 (CEL / jsep)
- [ ] 视图性能探针 & 监控面板
- [ ] AI 辅助:模型结构生成 / 视图建议 / 查询自然语言解析
17. FAQ
Q: 可以只用数据源而不使用内置视图吗?
A: 可以。直接 getEntityEngine().datasourceFactory.getDataSource() 调用 CRUD。
Q: 如何扩展一个新视图类型?
A: 实现 EntityView 接口(或继承内置基类),在 componentRegistry.registerView() 注册。
Q: 可以在多引擎实例间隔离吗?
A: 当前主设计是单例。后续将加入多实例 + context 方案。
Q: 引用是否支持附加元数据?
A: 当前结构最简。可在后端扩展引用表(例如增加 jsonb 列)并在数据源扩展。
Q: SSR 支持吗?
A: 是。服务端使用 EnginePrimitiveInitializer,客户端使用 createEntityEngineProvider。注意避免重复初始化。
18. License
MIT © scenemesh
如果你在集成上遇到困难,或希望我进一步为你的场景定制示例/脚手架,可直接在仓库发起 Issue 或讨论。
1. 注册额外视图与组件套件
import { EngineInitializer, getEntityEngine } from '@scenemesh/entity-engine';
class MySuiteAdapter { /* implements IEntityComponentSuiteAdapter */ }
class MyRenderer { name = 'toolbar-extra'; slotName = 'toolbar'; renderer = () => <div>Extra</div>; }
const initializer = new EngineInitializer([UserModel], [UserGridView], [new MySuiteAdapter()], [new MyRenderer()]);
await getEntityEngine(initializer);2. 访问数据源 CRUD
const engine = await getEntityEngine();
const ds = engine.datasourceFactory.getDataSource();
await ds.create({ modelName: 'user', data: { values: { name: 'Lucy', age: 20, role: 'guest' } } });
const { data, count } = await ds.findMany({ modelName: 'user', query: { pageIndex: 1, pageSize: 10 } });3. 动态生成默认视图
如果未显式注册某模型的某种 viewType(例如 grid),调用:
const v = engine.metaRegistry.findView('user', 'grid'); // 若无则按模型字段生成4. 条件显示 / 只读 / 必填逻辑
IEntityViewField 支持:
| 属性 | 含义 | 示例 |
| ---- | ---- | ---- |
| hiddenWhen | 条件表达式为 true 时隐藏 | role=="guest" |
| showWhen | 条件表达式为 true 时显示 | age>18 |
| readOnlyWhen | 条件成立时只读 | role=="admin" |
| disabledWhen | 条件成立时禁用 | age<1 |
| requiredWhen | 条件成立时必填 | role=="admin" |
(表达式解析器按需在上层应用实现,可替换为任意逻辑解析方案)
5. 在应用中通过 Provider 集成(Provider Integration)
apps/workbench/src/entity/provider 目录展示了如何在 Next.js (App Router) 环境中一次性封装引擎:
核心点:
- 使用
createEntityEngineProvider生成 React 上下文 Provider(内部完成EntityEngine初始化与依赖注入)。 - 注入多套组件套件:示例中同时使用
SemiSuiteAdapter与MUISuiteAdapter,并指定一个当前使用的套件suiteAdapter(可作为默认/首选套件描述)。 - 包裹在
TRPCReactProvider外层(或内层)以提供数据访问上下文。 - 自定义
router.navigate以适配 Next.js 的useRouter().push()。 - 提供
permissionGuard.checkPermission异步函数集中处理权限校验(此处示例直接放行)。 - 注册自定义
renderers(命名插槽扩展)以及内置EntityViewInspector以调试视图元数据。
示例代码(建议放在 src/entity-provider.tsx):
'use client';
import { useRouter } from 'next/navigation';
import { createEntityEngineProvider, EntityViewInspector } from '@scenemesh/entity-engine';
import { MUISuiteAdapter } from '@scenemesh/entity-suite-mui';
import { SemiSuiteAdapter } from '@scenemesh/entity-suite-semi';
import { TRPCReactProvider } from '@scenemesh/entity-engine/services/api/trpc/react';
import { models, views } from 'src/entity/model-config'; // 你的模型与视图配置
import React from 'react'
import { createEntityEngineProvider, EntityViewInspector, type IEntityNamedRenderer } from '@scenemesh/entity-engine'
// 你的模型与视图(最小示例)
const models = [ { name: 'user', title: '用户', fields: [ { name: 'id', title: 'ID', type: 'string', isPrimaryKey: true }, { name: 'name', title: '姓名', type: 'string' } ] } ] as any
const views = [ { name: 'user-grid', title: '用户列表', modelName: 'user', viewType: 'grid', items: [ { name: 'id' }, { name: 'name' } ] } ] as any
// 自定义插槽渲染器(可选)
const ViewToolbar: IEntityNamedRenderer = { name: 'view-tool-2', slotName: 'view-tool', renderer: (ctx) => <div>工具区 {ctx.model.name}</div> }
export function EntityEngineProvider({ children }: { children: React.ReactNode }) {
const router = useRouter()
// 使用 memo 避免每次渲染都重新创建 Provider 组件
const Provider = React.useMemo(() => createEntityEngineProvider({
models,
views,
suiteAdapter: { suiteName: 'build-in', suiteVersion: '1.0.0' },
router: { navigate: (path, state) => router.push(path) },
permissionGuard: { checkPermission: async () => true },
renderers: [ ViewToolbar, { ...EntityViewInspector } ],
serverInfo: { baseUrl: '', endpoint: '/api/ee' },
}), [router])
return <Provider>{children}</Provider>
}在页面中直接使用:
// app/layout.tsx 或某上层布局
import { EntityEngineProvider } from 'src/entity-provider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh-CN">
<body>
<EntityEngineProvider>
{children}
</EntityEngineProvider>
</body>
</html>
);
}这样任意子组件即可通过(假设库已导出相应 hooks,如未来 useEntityEngine())获取引擎实例与上下文数据,实现:
- 视图渲染(Grid/Form/MasterDetail)
- 数据 CRUD / 引用查询
- 自定义插槽渲染扩展(toolbar / shell settings)
- 权限与路由统一入口
若需按租户/用户隔离实例,可在
createEntityEngineProvider外再包一层,根据参数动态构建不同 initializer。
查询与过滤 (Query & Filter)
查询结构:IEntityQuery。
type IEntityQuery = {
pageSize?: number;
pageIndex?: number;
sortBy?: Record<string, 'asc' | 'desc'>;
references?: { fromModelName; fromFieldName; fromObjectId; toModelName };
filter?: IEntityQueryItem; // 递归 AND / OR / NOT 结构
}叶子条件:
{ field: 'age', operator: QueryOperator.GT, value: 18 }复合条件:
{
and: [
{ field: 'role', operator: QueryOperator.EQ, value: 'admin' },
{ or: [
{ field: 'age', operator: QueryOperator.GT, value: 30 },
{ field: 'age', operator: QueryOperator.IS_NULL, value: null }
]
}
]
}支持操作符(QueryOperator):EQ, NE, GT, GTE, LT, LTE, CONTAINS, STARTS_WITH, ENDS_WITH, IN, NOT_IN, BETWEEN, IS_NULL, IS_NOT_NULL 等。
引擎可通过 modelDelegate.getQueryMeta() 给 UI 构建器提供字段支持的操作符列表及枚举选项。
组件与视图扩展 (Views & Widgets Extensibility)
新增视图实现
import { EntityView } from '@scenemesh/entity-engine';
export class TimelineView extends EntityView {
readonly info = { viewName: 'timeline', displayName: '时间线视图' };
readonly Component = (props) => <div>TODO Timeline of {props.model.name}</div>;
}
// 注册
engine.componentRegistry.registerView(new TimelineView());自定义 Widget
import { EntityWidget } from '@scenemesh/entity-engine';
class BadgeWidget extends EntityWidget {
readonly info = { widgetName: 'badge', displayName: '徽章' };
readonly Component = ({ value }) => <span className="badge">{value}</span>;
}
class BadgeSuiteAdapter { // implements IEntityComponentSuiteAdapter
suiteName = 'badge-suite'; suiteVersion = '0.0.1';
private widgets = new Map([['badge', new BadgeWidget()]]);
getWidget(n){ return this.widgets.get(n); }
getWidgets(){ return [...this.widgets.values()]; }
}
engine.componentRegistry.registerAdapter(new BadgeSuiteAdapter());命名 Renderer (Slot)
engine.componentRegistry.registerRenderer({
name: 'grid-toolbar-extra',
slotName: 'grid.toolbar.right',
renderer: () => <button>导出 CSV</button>
});字段类型扩展 (Field Type Extension)
实现 IModelFieldTyper:
import { z } from 'zod';
class GeoPointFieldTyper { // implements IModelFieldTyper
get title(){ return '地理点'; }
get type(){ return 'geopoint'; }
get icon(){ return 'map_pin'; }
get description(){ return '经纬度'; }
getDefaultValue(){ return { lat: 0, lng: 0 }; }
getDefaultSchema(){ return z.object({ lat: z.number(), lng: z.number() }); }
getQueryItemMeta(field){ return { field, operators: [/* ... */] }; }
getDefaultWidgetType(viewType:string){ return 'map'; }
}
engine.fieldTyperRegistry.registerFieldTyper(new GeoPointFieldTyper());随后在模型字段中使用 type: 'geopoint'。
数据源扩展 (DataSource Extension)
默认实现:TRPCEntityObjectDataSource。
自定义:
class RestEntityDataSource { // implements IEntityDataSource
async findOne({ id }) { /* fetch(`/api/entity/${id}`) */ }
async findMany({ modelName, query }) { /* ... */ return { data: [], count: 0 }; }
// 其余方法按接口补全
}
class MyDataSourceFactory { // implements IEntityDataSourceFactory
private ds = new RestEntityDataSource();
getDataSource(){ return this.ds; }
}
// 在初始化时替换
// 方式一:fork 引擎创建逻辑
// 方式二:后续扩展 createEntityEngine 暴露工厂注入(TODO: 参见 Roadmap)校验与默认值 (Validation & Defaults)
- 字段可以直接提供
schema: ZodTypeAny;否则按类型自动推导。 - 模型委托
EntityModelDelegate.schema聚合所有字段 schema -> 用于 form 校验。 - 未提供值时,通过
toSupplementedValues使用字段默认值或类型默认值填充。
注意:复杂业务校验(交叉字段、异步校验)可在上层表单库 / 后端再补充。
tRPC 集成 (tRPC Integration)
文件:src/services/api/trpc/utils.ts
export const vanillaTrpcClient = createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url, transformer: superjson }) ] });数据源 TRPCEntityObjectDataSource 通过 vanillaTrpcClient.model.* 调用,例如:
await vanillaTrpcClient.model.listObjects.query({ modelName, pagination, reference, filter });SSR 关闭 (
ssr: false),如需要服务端渲染可扩展配置。
常用接口参考 (API Reference Snapshot)
摘要列出关键接口字段,详见源码
src/**。
IEntityModel
interface IEntityModel { name: string; title: string; description?: string; fields: IEntityField[] }IEntityField
interface IEntityField { name; title; type; isRequired?; isPrimaryKey?; isUnique?; searchable?; defaultValue?; refModel?; refField?; schema?; order?; }IEntityView
interface IEntityView { name; title; modelName; viewType; items: IEntityViewField[]; density?; canEdit?; canNew?; canDelete?; }IEntityDataSource (节选)
interface IEntityDataSource {
findOne({ id }): Promise<IEntityObject | null | undefined>;
findMany({ modelName, query }): Promise<{ data: IEntityObject[]; count: number }>;
create({ modelName, data, reference? }): Promise<IEntityObject | null | undefined>;
update({ id, data }): Promise<boolean>;
delete({ id }): Promise<boolean>;
}Roadmap
| 状态 | 目标 | 说明 |
| ---- | ---- | ---- |
| ✅ | 基础引擎单例 / 模型 / 视图补全 | 当前实现 |
| ✅ | tRPC 数据源 CRUD & 引用 & 树 | TRPCEntityObjectDataSource |
| ✅ | 内置视图 (form / grid / mastail / shell) | 可扩展注册更多 |
| ⏳ | 数据源注入定制 API | 允许外部直接提供自定义 factory |
| ⏳ | 视图运行时条件解析器抽象 | 允许替换表达式解析策略 |
| ⏳ | 字段类型可配置默认 WidgetMap | 细粒度控制 per viewType |
| ⏳ | 国际化 (i18n) 插件化 | 与 locales 目录集成 |
| ⏳ | 权限与操作策略接口 | 结合 permission.types |
| ⏳ | 测试覆盖提升 | Unit + Integration + 可视回归 |
欢迎在 Issue / PR 中补充你的需求。
贡献指南 (Contributing)
Fork / 新建分支:
feat/x、fix/x。安装依赖并执行
yarn build确保通过。提交前运行:
yarn lint && yarn fm:check附带最小复现或使用示例。
遵循语义化提交(建议):
feat: .../fix: .../docs: ...等。
许可 (License)
当前 package.json 标记为 private: true,尚未明确开源 License。若计划公开发布,建议:
- 选择合适协议(MIT / Apache-2.0 / MPL-2.0 等)。
- 添加
LICENSE文件并在package.json中声明"license": "MIT"(示例)。 - 更新本 README 中的许可章节。
常见问题 (FAQ)
| 问题 | 说明 |
| ---- | ---- |
| 为什么需要 EngineInitializer? | 保证首次创建时批量注册,避免重复注册与竞态。 |
| 可以二次调用 getEntityEngine(initializer)? | 第二次会忽略传入的 initializer(已存在实例);如需重置需扩展清理逻辑。 |
| 未定义视图时字段顺序如何? | 使用字段的 order,缺省为 0;同序时按声明顺序。 |
| 查询表达式如何解析? | 核心未内置解析器,留给上层(前端构造统一结构后传至后端)。 |
| 如何做权限控制? | 未来 Roadmap 中将引入权限接口,可对模型 / 字段 / 动作进行裁剪。 |
致谢 (Acknowledgements)
本项目借鉴了多种低代码 / 元数据驱动思想(如 Ad Hoc Admin、Headless CMS、tRPC 模式),感谢社区生态。
如有问题或改进建议,欢迎提交 Issue / PR。
Keep building data-driven UI with less boilerplate.
