feat: update docs (#990)

* feat: improve code

* feat: update docs

* feat: update docs

* Update index.md

* Update features.md

* Update when.md

* Update contributing.md

* Update translations.md

* feat: clean up

* Add files via upload

* Update the-first-app.md

* Update plugins.md

* Update a-b-c.md

* Update blocks.md

* feat: update docs

* Add files via upload

* Update charts.md

* feat: update navs

* Update index.md

* Update index.md

* Update features.md

* Update index.md

* Update docker-compose.md

* Update create-nocobase-app.md

* Update git-clone.md

* Update contributing.md

* Update translations.md

* Update plugins.md

* Update the-first-app.md

* Add files via upload

* Update charts.md

* Update charts.md

* Update a-b-c.md

* Update collections.md

* Update menus.md

* Update menus.md

Co-authored-by: Zhou <zhou.working@gmail.com>
This commit is contained in:
chenos 2022-10-31 11:52:17 +08:00 committed by GitHub
parent 8df527f7e9
commit a6eebb940f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
317 changed files with 12990 additions and 1905 deletions

View File

@ -76,6 +76,33 @@ const parseMenuItems = (items: any[], lang: string) => {
return menuItems;
};
const navs = [
{
title: 'Welcome',
'title.zh-CN': '欢迎',
path: '/welcome',
},
{
title: 'User manual',
'title.zh-CN': '使用手册',
path: '/manual',
},
{
title: 'Plugin Development',
'title.zh-CN': '插件开发',
path: '/development',
},
{
title: 'API reference',
'title.zh-CN': 'API 参考',
path: '/api',
},
{
title: 'GitHub',
path: 'https://github.com/nocobase/nocobase',
},
];
export default defineConfig({
title: 'NocoBase',
outputPath: `./docs/dist/${lang}`,
@ -87,66 +114,12 @@ export default defineConfig({
hash: true,
logo: 'https://www.nocobase.com/images/logo.png',
navs: {
'en-US': [
{
title: 'Introduction',
path: '/introduction'
},
{
title: 'Getting started',
path: '/getting-started'
},
{
title: 'Manual',
path: '/manual'
},
{
title: 'Development',
path: '/development'
},
{
title: 'API reference',
path: '/api'
},
{
title: 'GitHub',
path: 'https://github.com/nocobase/nocobase',
},
],
'zh-CN': [
{
title: '欢迎',
path: '/welcome'
},
// {
// title: '快速开始',
// path: '/getting-started'
// },
{
title: '使用手册',
path: '/manual'
},
{
title: '插件开发',
path: '/development'
},
{
title: 'API 参考',
path: '/api'
},
// {
// title: '社区',
// path: '/community'
// },
{
title: 'GitHub',
path: 'https://github.com/nocobase/nocobase',
},
]
'en-US': navs,
'zh-CN': navs.map((item) => ({ ...item, title: item['title.zh-CN'] || item.title })),
},
menus: Object.keys(menus).reduce((result, key) => {
const items = menus[key];
result[key] = parseMenuItems(items, lang);
return result;
}, {})
}, {}),
});

View File

@ -0,0 +1,57 @@
# ACLResource
ACLResourceACL 系统中的资源类。在 ACL 系统中,为用户授予权限时会自动创建对应的资源。
## 类方法
### `constructor()`
构造函数
**签名**
* `constructor(options: AclResourceOptions)`
**类型**
```typescript
type ResourceActions = { [key: string]: RoleActionParams };
interface AclResourceOptions {
name: string; // 资源名称
role: ACLRole; // 资源所属角色
actions?: ResourceActions;
}
```
**详细信息**
`RoleActionParams`详见 [`aclRole.grantAction`](./acl-role.md#grantaction)
### `getActions()`
获取资源的所有 Action返回结果为 `ResourceActions` 对象。
### `getAction()`
根据名称返回 Action 的参数配置,返回结果为 `RoleActionParams` 对象。
**详细信息**
`RoleActionParams`详见 [`aclRole.grantAction`](./acl-role.md#grantaction)
### `setAction()`
在资源内部设置一个 Action 的参数配置,返回结果为 `RoleActionParams` 对象。
**签名**
* `setAction(name: string, params: RoleActionParams)`
**详细信息**
* name - 要设置的 action 名称
* `RoleActionParams`详见 [`aclRole.grantAction`](./acl-role.md#grantaction)
### `setActions()`
**签名**
* `setActions(actions: ResourceActions)`
批量调用 `setAction` 的便捷方法

View File

@ -0,0 +1,87 @@
# ACL Role
ACLRoleACL 系统中的用户角色类。在 ACL 系统中,通常使用 `acl.define` 定义角色。
## 类方法
### `constructor()`
构造函数
**签名**
* `constructor(public acl: ACL, public name: string)`
**详细信息**
* acl - ACL 实例
* name - 角色名称
### `grantAction()`
为角色授予 Action 权限
**签名**
* `grantAction(path: string, options?: RoleActionParams)`
**类型**
```typescript
interface RoleActionParams {
fields?: string[];
filter?: any;
own?: boolean;
whitelist?: string[];
blacklist?: string[];
[key: string]: any;
}
```
**详细信息**
* path - 资源Action路径`posts:edit`,表示 `posts` 资源的 `edit` Action, 资源名称和 Action 之间使用 `:` 冒号分隔。
RoleActionParams 为授权时,对应 action 的可配置参数,用以实现更细粒度的权限控制。
* fields - 可访问的字段
```typescript
acl.define({
role: 'admin',
actions: {
'posts:view': {
// admin 用户可以请求 posts:view action但是只有 fields 配置的字段权限
fields: ["id", "title", "content"],
},
},
});
```
* filter - 权限资源过滤配置
```typescript
acl.define({
role: 'admin',
actions: {
'posts:view': {
// admin 用户可以请求 posts:view action但是列出的结果必须满足 filter 设置的条件。
filter: {
createdById: '{{ ctx.state.currentUser.id }}', // 支持模板语法,可以取 ctx 中的值,将在权限判断时替换
},
},
},
});
```
* own - 是否只能访问自己的数据
```typescript
const actionsWithOwn = {
'posts:view': {
"own": true //
}
}
// 等价于
const actionsWithFilter = {
'posts:view': {
"filter": {
"createdById": "{{ ctx.state.currentUser.id }}"
}
}
}
```
* whitelist - 白名单,只有在白名单中的字段才能被访问
* blacklist - 黑名单,黑名单中的字段不能被访问

232
docs/en-US/api/acl/acl.md Normal file
View File

@ -0,0 +1,232 @@
# ACL
ACL 为 Nocobase 中的权限控制模块。在 ACL 中注册角色、资源以及配置相应权限之后,即可对角色进行权限判断。
## 概念解释
* 角色 (`ACLRole`):权限判断的对象
* 资源 (`ACLResource`):在 Nocobase ACL 中,资源通常对应一个数据库表,概念上可类比为 Restful API 中的 Resource。
* Action对资源的操作`create`、`read`、`update`、`delete` 等。
* 策略 (`ACLAvailableStrategy`): 通常每个角色都有自己的权限策略,策略中定义了默认情况下的用户权限。
* 授权:在 `ACLRole` 实例中调用 `grantAction` 函数,为角色授予 `Action` 的访问权限。
* 鉴权:在 `ACL` 实例中调用 `can` 函数,函数返回结果既为用户的鉴权结果。
## 类方法
### `constructor()`
构造函数,创建一个 `ACL` 实例。
```typescript
import { ACL } from '@nocobase/database';
const acl = new ACL();
```
### `define()`
定义一个 ACL 角色
**签名**
* `define(options: DefineOptions): ACLRole`
**类型**
```typescript
interface DefineOptions {
role: string;
allowConfigure?: boolean;
strategy?: string | AvailableStrategyOptions;
actions?: ResourceActionsOptions;
routes?: any;
}
```
**详细信息**
* `role` - 角色名称
```typescript
// 定义一个名称为 admin 的角色
acl.define({
role: 'admin',
});
```
* `allowConfigure` - 是否允许配置权限
* `strategy` - 角色的权限策略
* 可以为 `string`,为要使用的策略名,表示使用已定义的策略。
* 可以为 `AvailableStrategyOptions`,为该角色定义一个新的策略,参考[`setAvailableActions()`](#setavailableactions)。
* `actions` - 定义角色时,可传入角色可访问的 `actions` 对象,
之后会依次调用 `aclRole.grantAction` 授予资源权限。详见 [`aclRole.grantAction`](./acl-role.md#grantaction)
```typescript
acl.define({
role: 'admin',
actions: {
'posts:edit': {}
},
});
// 等同于
const role = acl.define({
role: 'admin',
});
role.grantAction('posts:edit', {});
```
### `getRole()`
根据角色名称返回已注册的角色对象
**签名**
* `getRole(name: string): ACLRole`
### `removeRole()`
根据角色名称移除角色
**签名**
* `removeRole(name: string)`
### `can()`
鉴权函数
**签名**
* `can(options: CanArgs): CanResult | null`
**类型**
```typescript
interface CanArgs {
role: string; // 角色名称
resource: string; // 资源名称
action: string; //操作名称
}
interface CanResult {
role: string; // 角色名称
resource: string; // 资源名称
action: string; // 操作名称
params?: any; // 注册权限时传入的参数
}
```
**详细信息**
`can` 方法首先会判断角色是否有注册对应的 `Action` 权限,如果没有则会去判断角色的 `strategy` 是否匹配。
调用返回为`null`时,表示角色无权限,反之返回 `CanResult`对象,表示角色有权限。
**示例**
```typescript
// 定义角色,注册权限
acl.define({
role: 'admin',
actions: {
'posts:edit': {
fields: ['title', 'content'],
},
},
});
const canResult = acl.can({
role: 'admin',
resource: 'posts',
action: 'edit',
});
/**
* canResult = {
* role: 'admin',
* resource: 'posts',
* action: 'edit',
* params: {
* fields: ['title', 'content'],
* }
* }
*/
acl.can({
role: 'admin',
resource: 'posts',
action: 'destroy',
}); // null
```
### `use()`
**签名**
* `use(fn: any)`
向 middlewares 中添加中间件函数。
### `middleware()`
返回一个中间件函数,用于在 `@nocobase/server` 中使用。使用此 `middleware` 之后,`@nocobase/server` 在每次请求处理之前都会进行权限判断。
### `allow()`
设置资源为可公开访问
**签名**
* `allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc)`
**类型**
```typescript
type ConditionFunc = (ctx: any) => Promise<boolean> | boolean;
```
**详细信息**
* resourceName - 资源名称
* actionNames - 资源动作名
* condition? - 配置生效条件
* 传入 `string`,表示使用已定义的条件,注册条件使用 `acl.allowManager.registerCondition` 方法。
```typescript
acl.allowManager.registerAllowCondition('superUser', async () => {
return ctx.state.user?.id === 1;
});
// 开放 users:list 的权限,条件为 superUser
acl.allow('users', 'list', 'superUser');
```
* 传入 ConditionFunc可接收 `ctx` 参数,返回 `boolean`,表示是否生效。
```typescript
// 当用户ID为1时可以访问 user:list
acl.allow('users', 'list', (ctx) => {
return ctx.state.user?.id === 1;
});
```
**示例**
```typescript
// 注册 users:login 可以被公开访问
acl.allow('users', 'login');
```
### `setAvailableActions()`
**签名**
* `setAvailableStrategy(name: string, options: AvailableStrategyOptions)`
注册一个可用的权限策略
**类型**
```typescript
interface AvailableStrategyOptions {
displayName?: string;
actions?: false | string | string[];
allowConfigure?: boolean;
resource?: '*';
}
```
**详细信息**
* displayName - 策略名称
* allowConfigure - 此策略是否拥有 **配置资源** 的权限,设置此项为`true`之后,请求判断在 `ACL` 中注册成为 `configResources` 资源的权限,会返回通过。
* actions - 策略内的 actions 列表,支持通配符 `*`
* resource - 策略内的 resource 定义,支持通配符 `*`

349
docs/en-US/api/actions.md Normal file
View File

@ -0,0 +1,349 @@
# 内置常用资源操作
针对常用的 CRUD 等数据资源的操作NocoBase 内置了对应操作方法,并通过数据表资源自动映射相关的操作。
所有的操作方法都是注册在 resourcer 实例上,也是标准兼容 Koa 的中间件函数(`(ctx, next) => Promise<void>`)。操作的参数由路由解析后附加在 `ctx.action` 对象上,后续参数相关介绍均基于此对象。
通常情况下无需直接调用内置的 action 方法,在需要扩展默认操作行为时,可以在自定义的操作方法内调用默认方法。
## 包结构
可通过以下方式引入相关实体:
```ts
import actions from '@nocobase/actions';
```
## 单一数据资源操作
### `list()`
获取数据列表。对应资源操作的 URL 为 `GET /api/<resource>:list`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | 过滤参数 |
| `fields` | `string[]` | - | 要获取的字段 |
| `except` | `string[]` | - | 要排除的字段 |
| `appends` | `string[]` | - | 要附加的关系字段 |
| `sort` | `string[]` | - | 排序参数 |
| `page` | `number` | 1 | 分页 |
| `pageSize` | `number` | 20 | 每页数据条数 |
**示例**
当需要提供一个查询数据列表的接口,但不是默认以 JSON 格式输出时,可以基于内置默认方法进行扩展:
```ts
import actions from '@nocobase/actions';
app.actions({
async ['books:list'](ctx, next) {
ctx.action.mergeParams({
except: ['content']
});
await actions.list(ctx, async () => {
const { rows } = ctx.body;
// transform JSON to CSV output
ctx.body = rows.map(row => Object.keys(row).map(key => row[key]).join(',')).join('\n');
ctx.type = 'text/csv';
await next();
});
}
});
```
请求示例,将获得 CSV 格式文件的返回:
```shell
curl -X GET http://localhost:13000/api/books:list
```
### `get()`
获取单条数据。对应资源操作的 URL 为 `GET /api/<resource>:get`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `filterByTk` | `number \| string` | - | 过滤主键 |
| `filter` | `Filter` | - | 过滤参数 |
| `fields` | `string[]` | - | 要获取的字段 |
| `except` | `string[]` | - | 要排除的字段 |
| `appends` | `string[]` | - | 要附加的关系字段 |
| `sort` | `string[]` | - | 排序参数 |
| `page` | `number` | 1 | 分页 |
| `pageSize` | `number` | 20 | 每页数据条数 |
**示例**
基于 NocoBase 内置的文件管理插件,可以扩展当客户端请求以资源标识下载一个文件时,返回文件流:
```ts
import path from 'path';
import actions from '@nocobase/actions';
import { STORAGE_TYPE_LOCAL } from '@nocobase/plugin-file-manager';
app.actions({
async ['attachments:get'](ctx, next) {
ctx.action.mergeParams({
appends: ['storage'],
});
await actions.get(ctx, async () => {
if (ctx.accepts('json', 'application/octet-stream') === 'json') {
return next();
}
const { body: attachment } = ctx;
const { storage } = attachment;
if (storage.type !== STORAGE_TYPE_LOCAL) {
return ctx.redirect(attachment.url);
}
ctx.body = fs.createReadStream(path.resolve(storage.options.documentRoot?, storage.path));
ctx.attachment(attachment.filename);
ctx.type = 'application/octet-stream';
await next();
});
}
});
```
请求示例,将获得文件流的返回:
```shell
curl -X GET -H "Accept: application/octet-stream" http://localhost:13000/api/attachments:get?filterByTk=1
```
### `create()`
创建单条数据。对应资源操作的 URL 为 `POST /api/<resource>:create`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `values` | `Object` | - | 要创建的数据 |
**示例**
类似文件管理插件,创建带有二进制内容的数据作为上传文件的附件:
```ts
import multer from '@koa/multer';
import actions from '@nocobase/actions';
app.actions({
async ['files:create'](ctx, next) {
if (ctx.request.type === 'application/json') {
return actions.create(ctx, next);
}
if (ctx.request.type !== 'multipart/form-data') {
return ctx.throw(406);
}
// 文件保存处理仅用 multer() 作为示例,不代表完整的逻辑
multer().single('file')(ctx, async () => {
const { file, body } = ctx.request;
const { filename, mimetype, size, path } = file;
ctx.action.mergeParams({
values: {
filename,
mimetype,
size,
path: file.path,
meta: typeof body.meta === 'string' ? JSON.parse(body.meta) : {};
}
});
await actions.create(ctx, next);
});
}
});
```
请求示例,可以创建文件表的普通数据,也可以含附件一起提交:
```shell
# 仅创建普通数据
curl -X POST -H "Content-Type: application/json" -d '{"filename": "some-file.txt", "mimetype": "text/plain", "size": 5, "url": "https://cdn.yourdomain.com/some-file.txt"}' "http://localhost:13000/api/files:create"
# 含附件一起提交
curl -X POST -F "file=@/path/to/some-file.txt" -F 'meta={"length": 100}' "http://localhost:13000/api/files:create"
```
### `update()`
更新一条或多条数据。对应的 URL 为 `PUT /api/<resource>:update`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | 过滤参数 |
| `filterByTk` | `number \| string` | - | 过滤主键 |
| `values` | `Object` | - | 更新数据值 |
注:参数中的 `filter``filterByTk` 至少提供一项。
**示例**
类似 `create()` 的例子,更新文件记录可以扩展为可携带二进制内容的数据作为更新的文件:
```ts
import multer from '@koa/multer';
import actions from '@nocobase/actions';
app.actions({
async ['files:update'](ctx, next) {
if (ctx.request.type === 'application/json') {
return actions.update(ctx, next);
}
if (ctx.request.type !== 'multipart/form-data') {
return ctx.throw(406);
}
// 文件保存处理仅用 multer() 作为示例,不代表完整的逻辑
multer().single('file')(ctx, async () => {
const { file, body } = ctx.request;
const { filename, mimetype, size, path } = file;
ctx.action.mergeParams({
values: {
filename,
mimetype,
size,
path: file.path,
meta: typeof body.meta === 'string' ? JSON.parse(body.meta) : {};
}
});
await actions.update(ctx, next);
});
}
});
```
请求示例,可以创建文件表的普通数据,也可以含附件一起提交:
```shell
# 仅创建普通数据
curl -X PUT -H "Content-Type: application/json" -d '{"filename": "some-file.txt", "mimetype": "text/plain", "size": 5, "url": "https://cdn.yourdomain.com/some-file.txt"}' "http://localhost:13000/api/files:update"
# 含附件一起提交
curl -X PUT -F "file=@/path/to/some-file.txt" -F 'meta={"length": 100}' "http://localhost:13000/api/files:update"
```
### `destroy()`
删除一条或多条数据。对应的 URL 为 `DELETE /api/<resource>:destroy`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | 过滤参数 |
| `filterByTk` | `number \| string` | - | 过滤主键 |
注:参数中的 `filter``filterByTk` 至少提供一项。
**示例**
类似对文件管理插件扩展一个删除文件数据也需要同时删除对应文件的操作处理:
```ts
import actions from '@nocobase/actions';
app.actions({
async ['files:destroy'](ctx, next) {
// const repository = getRepositoryFromParams(ctx);
// const { filterByTk, filter } = ctx.action.params;
// const items = await repository.find({
// fields: [repository.collection.filterTargetKey],
// appends: ['storage'],
// filter,
// filterByTk,
// context: ctx,
// });
// await items.reduce((promise, item) => promise.then(async () => {
// await item.removeFromStorage();
// await item.destroy();
// }), Promise.resolve());
await actions.destroy(ctx, async () => {
// do something
await next();
});
}
});
```
### `move()`
对应的 URL 为 `POST /api/<resource>:move`
此方法用于移动数据,调整数据的排序。例如在页面中,拖拽一个元素到另一个元素的上方或下方,可调用此方法实现顺序调整。
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
|----------|-------------| -- |---------------|
| `sourceId` | `targetKey` | - | 移动的元素ID |
| `targetId` | `targetKey` | - | 与移动元素交换位置的元素ID |
| `sortField` | `string` | `sort` | 排序存储的字段名 |
| `targetScope` | `string` | - | 排序的scope一个 resource 可以按照不同的 scope 排序 |
| `sticky` | `boolean` | - | 是否置顶移动的元素 |
| `method` | `insertAfter` \| `prepend` | - | 插入类型,插入目标元素之前还是之后 |
## 关系资源资源操作
### `add()`
添加与对象的关联关系,对应的 URL 为 `POST /api/<resource.assocition>:add`。适用于 `hasMany``belongsToMany` 关联。
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
|----------|-------------| --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | 添加的关联对象ID |
### `remove()`
移除与对象的关联关系,对应的 URL 为 `POST /api/<resource.assocition>:remove`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
|----------|-------------| --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | 移除的关联对象ID |
### `set()`
设置关联的关联对象,对应的 URL 为 `POST /api/<resource.assocition>:set`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
|----------|-------------| --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | 设置的关联对象的ID |
### `toggle()`
切换关联的关联对象,对应的 URL 为 `POST /api/<resource.assocition>:toggle`。`toggle` 在内部判断关联对象是否已经存在,如果存在则移除,如果不存在则添加。
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
|----------|-------------| -- | --- |
| `values` | `TargetKey` | - | 切换的关联对象的ID |

350
docs/en-US/api/cli.md Normal file
View File

@ -0,0 +1,350 @@
# NocoBase CLI
NocoBase CLI 旨在帮助你开发、构建和部署 NocoBase 应用。
<Alert>
NocoBase CLI 支持 ts-node 和 node 两种运行模式
- ts-node 模式(默认):用于开发环境,支持实时编译,但是响应较慢
- node 模式:用于生产环境,响应迅速,但需要先执行 `yarn nocobase build` 将全部源码进行编译
</Alert>
## 使用说明
```bash
$ yarn nocobase -h
Usage: nocobase [command] [options]
Options:
-h, --help
Commands:
console
db:auth 校验数据库是否连接成功
db:sync 通过 collections 配置生成相关数据表和字段
install 安装
start 生产环境启动应用
build 编译打包
clean 删除编译之后的文件
dev 启动应用,用于开发环境,支持实时编译
doc 文档开发
test 测试
umi
upgrade 升级
migrator 数据迁移
pm 插件管理器
help
```
## 在脚手架里应用
应用脚手架 `package.json` 里的 `scripts` 如下:
```json
{
"scripts": {
"dev": "nocobase dev",
"start": "nocobase start",
"clean": "nocobase clean",
"build": "nocobase build",
"test": "nocobase test",
"pm": "nocobase pm",
"postinstall": "nocobase postinstall"
}
}
```
## 命令行扩展
NocoBase CLI 基于 [commander](https://github.com/tj/commander.js) 构建,你可以自由扩展命令,扩展的 command 可以写在 `app/server/index.ts` 里:
```ts
const app = new Application(config);
app.command('hello').action(() => {});
```
或者,写在插件里:
```ts
class MyPlugin extends Plugin {
beforeLoad() {
this.app.command('hello').action(() => {});
}
}
```
终端运行
```bash
$ yarn nocobase hello
```
## 内置命令行
按使用频率排序
### `dev`
开发环境下,启动应用,代码实时编译。
<Alert>
NocoBase 未安装时,会自动安装(参考 install 命令)
</Alert>
```bash
Usage: nocobase dev [options]
Options:
-p, --port [port]
--client
--server
-h, --help
```
示例
```bash
# 启动应用,用于开发环境,实时编译
yarn nocobase dev
# 只启动服务端
yarn nocobase dev --server
# 只启动客户端
yarn nocobase dev --client
```
### `start`
生产环境下,启动应用,代码需要 yarn build。
<Alert>
- NocoBase 未安装时,会自动安装(参考 install 命令)
- 源码有修改时,需要重新打包(参考 build 命令)
</Alert>
```bash
$ yarn nocobase start -h
Usage: nocobase start [options]
Options:
-p, --port
-s, --silent
-h, --help
```
示例
```bash
# 启动应用,用于生产环境,
yarn nocobase start
```
### `install`
安装
```bash
$ yarn nocobase install -h
Usage: nocobase install [options]
Options:
-f, --force
-c, --clean
-s, --silent
-l, --lang [lang]
-e, --root-email <rootEmail>
-p, --root-password <rootPassword>
-n, --root-nickname [rootNickname]
-h, --help
```
示例
```bash
# 初始安装
yarn nocobase install -l zh-CN -e admin@nocobase.com -p admin123
# 删除 NocoBase 的所有数据表,并重新安装
yarn nocobase install -f -l zh-CN -e admin@nocobase.com -p admin123
# 清空数据库,并重新安装
yarn nocobase install -c -l zh-CN -e admin@nocobase.com -p admin123
```
<Alert>
`-f/--force``-c/--clean` 的区别
- `-f/--force` 删除 NocoBase 的数据表
- `-c/--clean` 清空数据库,所有数据表都会被删除
</Alert>
### `upgrade`
升级
```bash
yarn nocobase upgrade
```
### `test`
jest 测试,支持所有 [jest-cli](https://jestjs.io/docs/cli) 的 options除此之外还扩展了 `-c, --db-clean` 的支持。
```bash
$ yarn nocobase test -h
Usage: nocobase test [options]
Options:
-c, --db-clean 运行所有测试前清空数据库
-h, --help
```
示例
```bash
# 运行所有测试文件
yarn nocobase test
# 运行指定文件夹下所有测试文件
yarn nocobase test packages/core/server
# 运行指定文件里的所有测试
yarn nocobase test packages/core/database/src/__tests__/database.test.ts
# 运行测试前,清空数据库
yarn nocobase test -c
yarn nocobase test packages/core/server -c
```
### `build`
代码部署到生产环境前,需要将源码编译打包,如果代码有修改,也需要重新构建。
```bash
# 所有包
yarn nocobase build
# 指定包
yarn nocobase build app/server app/client
```
### `clean`
删除编译之后的文件
```bash
yarn clean
# 等同于
yarn rimraf -rf packages/*/*/{lib,esm,es,dist}
```
### `doc`
文档开发
```bash
# 启动文档
yarn doc --lang=zh-CN # 等同于 yarn doc dev
# 构建文档,默认输出到 ./docs/dist/ 目录下
yarn doc build
# 查看 dist 输出的文档最终效果
yarn doc serve --lang=zh-CN
```
### `db:auth`
校验数据库是否连接成功
```bash
$ yarn nocobase db:auth -h
Usage: nocobase db:auth [options]
Options:
-r, --retry [retry] 重试次数
-h, --help
```
### `db:sync`
通过 collections 配置生成数据表和字段
```bash
$ yarn nocobase db:sync -h
Usage: nocobase db:sync [options]
Options:
-f, --force
-h, --help display help for command
```
### `migrator`
数据迁移
```bash
$ yarn nocobase migrator
Positional arguments:
<command>
up Applies pending migrations
down Revert migrations
pending Lists pending migrations
executed Lists executed migrations
create Create a migration file
```
### `pm`
插件管理器
```bash
# 创建插件
yarn pm create hello
# 注册插件
yarn pm add hello
# 激活插件
yarn pm enable hello
# 禁用插件
yarn pm disable hello
# 删除插件
yarn pm remove hello
```
未实现
```bash
# 升级插件
yarn pm upgrade hello
# 发布插件
yarn pm publish hello
```
### `umi`
`app/client` 基于 [umi](https://umijs.org/) 构建,可以通过 `nocobase umi` 来执行其他相关命令。
```bash
# 生成开发环境所需的 .umi 缓存
yarn nocobase umi generate tmp
```
### `help`
帮助命令,也可以用 option 参数,`-h` 和 `--help`
```bash
# 查看所有 cli
yarn nocobase help
# 也可以用 -h
yarn nocobase -h
# 或者 --help
yarn nocobase --help
# 查看 db:sync 命令的 option
yarn nocobase db:sync -h
```

View File

@ -0,0 +1,62 @@
# Application
## 构造函数
### `constructor()`
创建一个应用实例。
**签名**
* `constructor(options: ApplicationOptions)`
**示例**
```ts
const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`../plugins/${name}`);
},
});
```
## 方法
### use()
添加 Providers内置 Providers 有:
- APIClientProvider
- I18nextProvider
- AntdConfigProvider
- RemoteRouteSwitchProvider
- SystemSettingsProvider
- PluginManagerProvider
- SchemaComponentProvider
- SchemaInitializerProvider
- BlockSchemaComponentProvider
- AntdSchemaComponentProvider
- ACLProvider
- RemoteDocumentTitleProvider
### render()
渲染 App 组件
```ts
import { Application } from '@nocobase/client';
export const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`../plugins/${name}`);
},
});
export default app.render();
```

View File

@ -0,0 +1,23 @@
# ACL
## Components
### `<ACLProvider />`
### `<ACLRolesCheckProvider />`
### `<ACLCollectionProvider />`
### `<ACLActionProvider />`
### `<ACLCollectionFieldProvider />`
### `<ACLMenuItemProvider />`
## Hooks
### `useACLContext()`
### `useACLRoleContext()`
### `useRoleRecheck()`

View File

@ -0,0 +1,25 @@
# BlockProvider
## 内核方法
### `<BlockProvider />`
### `useBlockRequestContext()`
## 内置 BlockProvider 组件
### `<CalendarBlockProvider />`
### `<TableFieldProvider />`
### `<TableBlockProvider />`
### `<TableSelectorProvider />`
### `<FormBlockProvider />`
### `<FormFieldProvider />`
### `<DetailsBlockProvider />`
### `<KanbanBlockProvider />`

View File

@ -0,0 +1,267 @@
# CollectionManager
## Components
### CollectionManagerProvider
```jsx | pure
<CollectionManagerProvider interfaces={{}} collections={[]}></CollectionManagerProvider>
```
### CollectionProvider
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}></CollectionProvider>
```
如果没有传 collection 参数,从 CollectionManagerProvider 里取对应 name 的 collection。
```jsx | pure
const collections = [
{
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
}
];
<CollectionManagerProvider collections={collections}>
<CollectionProvider name={'tests'}></CollectionProvider>
</CollectionManagerProvider>
```
### CollectionFieldProvider
```jsx | pure
const field = {
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
};
<CollectionFieldProvider field={field}></CollectionFieldProvider>
```
如果没有传 field 参数,从 CollectionProvider 里取对应 name 的 field。
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}>
<CollectionFieldProvider name={'title'}></CollectionFieldProvider>
</CollectionProvider>
```
### CollectionField
万能字段组件,需要与 `<CollectionProvider/>` 搭配使用,仅限于在 Schema 场景使用。从 CollectionProvider 里取对应 name 的 field schema。可通过 CollectionField 所在的 schema 扩展配置。
```ts
{
name: 'title',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'CollectionField',
'x-component-props': {},
properties: {},
}
```
## Hooks
### useCollectionManager()
`<CollectionManagerProvider/>` 搭配使用
```jsx | pure
const { collections, get } = useCollectionManager();
```
### useCollection()
`<CollectionProvider/>` 搭配使用
```jsx | pure
const { name, fields, getField, findField, resource } = useCollection();
```
### useCollectionField()
`<CollectionFieldProvider/>` 搭配使用
```jsx | pure
const { name, uiSchema, resource } = useCollectionField();
```
resource 需要与 `<RecordProvider/>` 搭配使用,用于提供当前数据表行记录的上下文。如:
# CollectionManager
## Components
### CollectionManagerProvider
```jsx | pure
<CollectionManagerProvider interfaces={{}} collections={[]}></CollectionManagerProvider>
```
### CollectionProvider
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}></CollectionProvider>
```
如果没有传 collection 参数,从 CollectionManagerProvider 里取对应 name 的 collection。
```jsx | pure
const collections = [
{
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
}
];
<CollectionManagerProvider collections={collections}>
<CollectionProvider name={'tests'}></CollectionProvider>
</CollectionManagerProvider>
```
### CollectionFieldProvider
```jsx | pure
const field = {
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
};
<CollectionFieldProvider field={field}></CollectionFieldProvider>
```
如果没有传 field 参数,从 CollectionProvider 里取对应 name 的 field。
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}>
<CollectionFieldProvider name={'title'}></CollectionFieldProvider>
</CollectionProvider>
```
### CollectionField
万能字段组件,需要与 `<CollectionProvider/>` 搭配使用,仅限于在 Schema 场景使用。从 CollectionProvider 里取对应 name 的 field schema。可通过 CollectionField 所在的 schema 扩展配置。
```ts
{
name: 'title',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'CollectionField',
'x-component-props': {},
properties: {},
}
```
## Hooks
### useCollectionManager()
`<CollectionManagerProvider/>` 搭配使用
```jsx | pure
const { collections, get } = useCollectionManager();
```
### useCollection()
`<CollectionProvider/>` 搭配使用
```jsx | pure
const { name, fields, getField, findField, resource } = useCollection();
```
### useCollectionField()
`<CollectionFieldProvider/>` 搭配使用
```jsx | pure
const { name, uiSchema, resource } = useCollectionField();
```
resource 需要与 `<RecordProvider/>` 搭配使用,用于提供当前数据表行记录的上下文。

View File

@ -0,0 +1,14 @@
# 适配的 Schema 组件
## Common
- DndContext
- SortableItem
## And Design
- Action
- BlockItem
- Calendar
- CardItem
- Cascader

View File

@ -0,0 +1,3 @@
# Overview
test

View File

@ -0,0 +1,79 @@
# RouteSwitch
## `<RouteSwitchProvider />`
```ts
interface RouteSwitchProviderProps {
components?: ReactComponent;
routes?: RouteRedirectProps[];
}
```
## `<RouteSwitch />`
```ts
interface RouteSwitchProps {
routes?: RouteRedirectProps[];
components?: ReactComponent;
}
type RouteRedirectProps = RedirectProps | RouteProps;
interface RedirectProps {
type: 'redirect';
to: any;
path?: string;
exact?: boolean;
strict?: boolean;
push?: boolean;
from?: string;
[key: string]: any;
}
interface RouteProps {
type: 'route';
path?: string | string[];
exact?: boolean;
strict?: boolean;
sensitive?: boolean;
component?: any;
routes?: RouteProps[];
[key: string]: any;
}
```
## 完整示例
```tsx | pure
import React from 'react';
import { Link, MemoryRouter as Router } from 'react-router-dom';
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const routes: RouteRedirectProps[] = [
{
type: 'route',
path: '/',
exact: true,
component: 'Home',
},
{
type: 'route',
path: '/about',
component: 'About',
},
];
export default () => {
return (
<RouteSwitchProvider components={{ Home, About }}>
<Router initialEntries={['/']}>
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
<RouteSwitch routes={routes} />
</Router>
</RouteSwitchProvider>
);
};
```

View File

@ -0,0 +1,13 @@
# SchemaComponent
## 核心组件
### `<SchemaComponentProvider />`
### `<SchemaComponentOptions>`
### `<SchemaComponent>`
## 核心方法
### `createDesignable()`
### `useDesignable()`
### `useCompile()`

View File

@ -0,0 +1,27 @@
# SchemaInitializer
用于各种 schema 的初始化。新增的 schema 可以插入到某个已有 schema 节点的任意位置,包括:
```ts
{
properties: {
// beforeBegin 在当前节点的前面插入
node1: {
properties: {
// afterBegin 在当前节点的第一个子节点前面插入
// ...
// beforeEnd 在当前节点的最后一个子节点后面
},
},
// afterEnd 在当前节点的后面
},
}
```
SchemaInitializer 的核心包括 `<SchemaInitializer.Button />``<SchemaInitializer.Item />` 两个组件。`<SchemaInitializer.Button />` 用于创建 Schema 的下拉菜单按钮,下拉菜单的菜单项为 `<SchemaInitializer.Item/>`
### `<SchemaInitializerProvider />`
### `<SchemaInitializer.Button />`
### `<SchemaInitializer.Item/>`

View File

@ -0,0 +1,25 @@
# SchemaSettings
### `<SchemaSettings />`
### `<SchemaSettings.Item />`
### `<SchemaSettings.ItemGroup />`
### `<SchemaSettings.SubMenu />`
### `<SchemaSettings.Divider />`
### `<SchemaSettings.Remove />`
### `<SchemaSettings.SelectItem />`
### `<SchemaSettings.SwitchItem />`
### `<SchemaSettings.ModalItem />`
### `<SchemaSettings.ActionModalItem />`
### `<SchemaSettings.Template />`
### `<SchemaSettings.BlockTitleItem />`

View File

@ -0,0 +1,484 @@
# Collection
数据表结构管理类。
大部分接口通常不会直接由开发者调用,除非进行较底层的扩展开发。
## 构造函数
通常不会直接使用,主要通过 `Database` 实例的 `collection` 方法作为代理入口调用。
**签名**
* `constructor(options: CollectionOptions, context: CollectionContext)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.name` | `string` | - | collection 标识 |
| `options.tableName?` | `string` | - | 数据库表名,如不传则使用 `options.name` 的值 |
| `options.fields?` | `FieldOptions[]` | - | 字段定义,详见 [Field](./field) |
| `options.model?` | `string \| ModelCtor<Model>` | - | Sequelize 的 Model 类型,如果使用的是 `string`,则需要调用之前在 db 上注册过该模型名称 |
| `options.repository?` | `string \| RepositoryType` | - | 数据仓库类型,如果使用 `string`,则需要调用之前在 db 上注册过该仓库类型 |
| `options.sortable?` | `string \| boolean \| { name?: string; scopeKey?: string }` | - | 数据可排序字段配置,默认不排序 |
| `options.autoGenId?` | `boolean` | `true` | 是否自动生成唯一主键,默认为 `true` |
| `context.database` | `Database` | - | 所在的上下文环境数据库 |
**示例**
创建一张文章表:
```ts
const posts = new Collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'double',
name: 'price',
}
]
}, {
// 已存在的数据库实例
database: db
});
```
## 实例成员
### `options`
数据表配置初始参数。与构造函数的 `options` 参数一致。
### `context`
当前数据表所属的上下文环境,目前主要是数据库实例。
### `name`
数据表名称。
### `db`
所属数据库实例。
### `filterTargetKey`
作为主键的字段名。
### `isThrough`
是否为中间表。
### `model`
匹配 Sequelize 的 Model 类型。
### `repository`
数据仓库实例。
## 字段配置方法
### `getField()`
获取数据表已定义对应名称的字段对象。
**签名**
* `getField(name: string): Field`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 字段名称 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
const field = posts.getField('title');
```
### `setField()`
对数据表设置字段。
**签名**
* `setField(name: string, options: FieldOptions): Field`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 字段名称 |
| `options` | `FieldOptions` | - | 字段配置,详见 [Field](./field) |
**示例**
```ts
const posts = db.collection({ name: 'posts' });
posts.setField('title', { type: 'string' });
```
### `setFields()`
对数据表批量设置多个字段。
**签名**
* `setFields(fields: FieldOptions[], resetFields = true): Field[]`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `fields` | `FieldOptions[]` | - | 字段配置,详见 [Field](./field) |
| `resetFields` | `boolean` | `true` | 是否重置已存在的字段 |
**示例**
```ts
const posts = db.collection({ name: 'posts' });
posts.setFields([
{ type: 'string', name: 'title' },
{ type: 'double', name: 'price' }
]);
```
### `removeField()`
移除数据表已定义对应名称的字段对象。
**签名**
* `removeField(name: string): void | Field`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 字段名称 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.removeField('title');
```
### `resetFields()`
重置(清空)数据表的字段。
**签名**
* `resetFields(): void`
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.resetFields();
```
### `hasField()`
判断数据表是否已定义对应名称的字段对象。
**签名**
* `hasField(name: string): boolean`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 字段名称 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.hasField('title'); // true
```
### `findField()`
查找数据表中符合条件的字段对象。
**签名**
* `findField(predicate: (field: Field) => boolean): Field | undefined`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `predicate` | `(field: Field) => boolean` | - | 查找条件 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.findField(field => field.name === 'title');
```
### `forEachField()`
遍历数据表中的字段对象。
**签名**
* `forEachField(callback: (field: Field) => void): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `callback` | `(field: Field) => void` | - | 回调函数 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.forEachField(field => console.log(field.name));
```
## 索引配置方法
### `addIndex()`
添加数据表索引。
**签名**
* `addIndex(index: string | string[] | { fields: string[], unique?: boolean,[key: string]: any })`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `index` | `string \| string[]` | - | 需要配置索引的字段名 |
| `index` | `{ fields: string[], unique?: boolean, [key: string]: any }` | - | 完整配置 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.addIndex({
fields: ['title'],
unique: true
});
```
### `removeIndex()`
移除数据表索引。
**签名**
* `removeIndex(fields: string[])`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `fields` | `string[]` | - | 需要移除索引的字段名组合 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
],
indexes: [
{
fields: ['title'],
unique: true
}
]
});
posts.removeIndex(['title']);
```
## 表配置方法
### `remove()`
删除数据表。
**签名**
* `remove(): void`
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.remove();
```
## 数据库操作方法
### `sync()`
同步数据表定义到数据库。除了 Sequelize 中默认的 `Model.sync` 的逻辑,还会一并处理关系字段对应的数据表。
**签名**
* `sync(): Promise<void>`
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
await posts.sync();
```
### `existsInDb()`
判断数据表是否存在于数据库中。
**签名**
* `existsInDb(options?: Transactionable): Promise<boolean>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options?.transaction` | `Transaction` | - | 事务实例 |
**示例**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
const existed = await posts.existsInDb();
console.log(existed); // false
```
### `removeFromDb()`
**签名**
* `removeFromDb(): Promise<void>`
**示例**
```ts
const books = db.collection({
name: 'books'
});
// 同步书籍表到数据库
await db.sync();
// 删除数据库中的书籍表
await books.removeFromDb();
```

View File

@ -0,0 +1,554 @@
# Field
数据表字段管理类(抽象类)。同时是所有字段类型的基类,其他任意字段类型均通过继承该类来实现。
## 构造函数
通常不会直接由开发者调用,主要通过 `db.collection({ fields: [] })` 方法作为代理入口调用。
在扩展字段时主要通过继承 `Field` 抽象类,再注册到 Database 实例中来实现。
**签名**
* `constructor(options: FieldOptions, context: FieldContext)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `FieldOptions` | - | 字段配置对象 |
| `options.name` | `string` | - | 字段名称 |
| `options.type` | `string` | - | 字段类型,对应在 db 中注册的字段类型名称 |
| `context` | `FieldContext` | - | 字段上下文对象 |
| `context.database` | `Database` | - | 数据库实例 |
| `context.collection` | `Collection` | - | 数据表实例 |
## 实例成员
### `name`
字段名称。
### `type`
字段类型。
### `dataType`
字段数据库存储类型。
### `options`
字段初始化配置参数。
### `context`
字段上下文对象。
## 配置方法
### `on()`
基于数据表事件的快捷定义方式。相当于 `db.on(this.collection.name + '.' + eventName, listener)`
继承时通常无需覆盖此方法。
**签名**
* `on(eventName: string, listener: (...args: any[]) => void)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `eventName` | `string` | - | 事件名称 |
| `listener` | `(...args: any[]) => void` | - | 事件监听器 |
### `off()`
基于数据表事件的快捷移除方式。相当于 `db.off(this.collection.name + '.' + eventName, listener)`
继承时通常无需覆盖此方法。
**签名**
* `off(eventName: string, listener: (...args: any[]) => void)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `eventName` | `string` | - | 事件名称 |
| `listener` | `(...args: any[]) => void` | - | 事件监听器 |
### `bind()`
当字段被添加到数据表时触发的执行内容。通常用于添加数据表事件监听器和其他处理。
继承时需要先调用对应的 `super.bind()` 方法。
**签名**
* `bind()`
### `unbind()`
当字段从数据表中移除时触发的执行内容。通常用于移除数据表事件监听器和其他处理。
继承时需要先调用对应的 `super.unbind()` 方法。
**签名**
* `unbind()`
### `get()`
获取字段的配置项的值。
**签名**
* `get(key: string): any`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `key` | `string` | - | 配置项名称 |
**示例**
```ts
const field = db.collection('users').getField('name');
// 获取字段名称配置项的值,返回 'name'
console.log(field.get('name'));
```
### `merge()`
合并字段的配置项的值。
**签名**
* `merge(options: { [key: string]: any }): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `{ [key: string]: any }` | - | 要合并的配置项对象 |
**示例**
```ts
const field = db.collection('users').getField('name');
field.merge({
// 添加一个索引配置
index: true
});
```
### `remove()`
从数据表中移除字段(仅从内存中移除)。
**示例**
```ts
const books = db.getCollections('books');
books.getField('isbn').remove();
// really remove from db
await books.sync();
```
## 数据库方法
### `removeFromDb()`
从数据库中移除字段。
**签名**
* `removeFromDb(options?: Transactionable): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.transaction?` | `Transaction` | - | 事务实例 |
### `existsInDb()`
判断字段是否存在于数据库中。
**签名**
* `existsInDb(options?: Transactionable): Promise<boolean>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.transaction?` | `Transaction` | - | 事务实例 |
## 内置字段类型列表
NocoBase 内置了一些常用的字段类型,可以直接在定义数据表的字段是使用对应的 type 名称来指定类型。不同类型的字段参数配置不同,具体可参考下面的列表。
所有字段类型的配置项除了以下额外介绍的以外,都会透传至 Sequelize所以所有 Sequelize 支持的字段配置项都可以在这里使用(如 `allowNull`、`defaultValue` 等)。
另外 server 端的字段类型主要解决数据库存储和部分算法的问题,与前端的字段展示类型和使用组件基本无关。前端字段类型可以参考教程对应说明。
### `'boolean'`
逻辑值类型。
**示例**
```js
db.collection({
name: 'books',
fields: [
{
type: 'boolean',
name: 'published'
}
]
});
```
### `'integer'`
整型32 位)。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'integer',
name: 'pages'
}
]
});
```
### `'bigInt'`
长整型64 位)。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'bigInt',
name: 'words'
}
]
});
```
### `'double'`
双精度浮点型64 位)。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'double',
name: 'price'
}
]
});
```
### `'real'`
实数类型(仅 PG 适用)。
### `'decimal'`
十进制小数类型。
### `'string'`
字符串类型。相当于大部分数据库的 `VARCHAR` 类型。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'string',
name: 'title'
}
]
});
```
### `'text'`
文本类型。相当于大部分数据库的 `TEXT` 类型。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'text',
name: 'content'
}
]
});
```
### `'password'`
密码类型NocoBase 扩展)。基于 Node.js 原生的 crypto 包的 `scrypt` 方法进行密码加密。
**示例**
```ts
db.collection({
name: 'users',
fields: [
{
type: 'password',
name: 'password',
length: 64, // 长度,默认 64
randomBytesSize: 8 // 随机字节长度,默认 8
}
]
});
```
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `length` | `number` | 64 | 字符长度 |
| `randomBytesSize` | `number` | 8 | 随机字节大小 |
### `'date'`
日期类型。
### `'time'`
时间类型。
### `'array'`
数组类型(仅 PG 适用)。
### `'json'`
JSON 类型。
### `'jsonb'`
JSONB 类型(仅 PG 适用,其他会被兼容为 `'json'` 类型)。
### `'uuid'`
UUID 类型。
### `'uid'`
UID 类型NocoBase 扩展)。短随机字符串标识符类型。
### `'formula'`
公式类型NocoBase 扩展)。可配置基于 [mathjs](https://www.npmjs.com/package/mathjs) 的数学公式计算,公式中可以引用同一条记录中其他列的数值参与计算。
**示例**
```ts
db.collection({
name: 'orders',
fields: [
{
type: 'double',
name: 'price'
},
{
type: 'integer',
name: 'quantity'
},
{
type: 'formula',
name: 'total',
expression: 'price * quantity'
}
]
});
```
### `'radio'`
单选类型NocoBase 扩展)。全表最多有一行数据的该字段值为 `true`,其他都为 `false``null`
**示例**
整个系统只有一个被标记为 root 的用户,任意另一个用户的 root 值被改为 `true` 之后,其他所有 root 为 `true` 的记录均会被修改为 `false`
```ts
db.collection({
name: 'users',
fields: [
{
type: 'radio',
name: 'root',
}
]
});
```
### `'sort'`
排序类型NocoBase 扩展)。基于整型数字进行排序,为新记录自动生成新序号,当移动数据时进行序号重排。
数据表如果定义了 `sortable` 选项,也会自动生成对应字段。
**示例**
文章基于所属用户可排序:
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'user',
},
{
type: 'sort',
name: 'priority',
scopeKey: 'userId' // 以 userId 相同值分组的数据进行排序
}
]
});
```
### `'virtual'`
虚拟类型。不实际储存数据,仅用于特殊 getter/setter 定义时使用。
### `'belongsTo'`
多对一关联类型。外键储存在自身表,与 hasOne/hasMany 相对。
**示例**
任意文章属于某个作者:
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'author',
target: 'users', // 不配置默认为 name 复数名称的表名
foreignKey: 'authorId', // 不配置默认为 <name> + Id 的格式
sourceKey: 'id' // 不配置默认为 target 表的 id
}
]
});
```
### `'hasOne'`
一对一关联类型。外键储存在关联表,与 belongsTo 相对。
**示例**
任意用户都有一份个人资料:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasOne',
name: 'profile',
target: 'profiles', // 可省略
}
]
})
```
### `'hasMany'`
一对多关联类型。外键储存在关联表,与 belongsTo 相对。
**示例**
任意用户可以拥有多篇文章:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'posts',
foreignKey: 'authorId',
sourceKey: 'id'
}
]
});
```
### `'belongsToMany'`
多对多关联类型。使用中间表储存双方外键,如不指定已存在的表为中间表的话,将会自动创建中间表。
**示例**
任意文章可以加任意多个标签,任意标签也可以被任意多篇文章添加:
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsToMany',
name: 'tags',
target: 'tags', // 同名可省略
through: 'postsTags', // 中间表不配置将自动生成
foreignKey: 'postId', // 自身表在中间表的外键
sourceKey: 'id', // 自身表的主键
otherKey: 'tagId' // 关联表在中间表的外键
}
]
});
db.collection({
name: 'tags',
fields: [
{
type: 'belongsToMany',
name: 'posts',
through: 'postsTags', // 同一组关系指向同一张中间表
}
]
});
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,816 @@
# Filter Operators
用于 Repository 的 find、findOne、findAndCount、count 等 API 的 filter 参数里。如:
```ts
const repository = db.getRepository('books');
repository.find({
filter: {
title: {
$eq: '春秋',
}
}
});
```
相当于 Sequelize Where 查询的 [Op](https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#operators) 对象。
为了支持 JSON 化NocoBase 中将查询运算符转换为以 $ 为前缀的字符串标识。
另外NocoBase 也提供了扩展运算符的 API详见 [`db.registerOperators()`](../database#registeroperators)。
## 通用运算符
### `$eq`
判断字段值是否相等于指定值。相当于 SQL 的 `=`
**示例**
```ts
repository.find({
filter: {
title: {
$eq: '春秋',
}
}
});
```
等同于 `title: '春秋'`
### `$ne`
判断字段值是否不等于指定值。相当于 SQL 的 `!=`
**示例**
```ts
repository.find({
filter: {
title: {
$ne: '春秋',
}
}
});
```
### `$is`
判断字段值是否为指定值。相当于 SQL 的 `IS`
**示例**
```ts
repository.find({
filter: {
title: {
$is: null,
}
}
});
```
### `$not`
判断字段值是否不为指定值。相当于 SQL 的 `IS NOT`
**示例**
```ts
repository.find({
filter: {
title: {
$not: null,
}
}
});
```
### `$col`
判断字段值是否等于另一个字段的值。相当于 SQL 的 `=`
**示例**
```ts
repository.find({
filter: {
title: {
$col: 'name',
}
}
});
```
### `$in`
判断字段值是否在指定数组中。相当于 SQL 的 `IN`
**示例**
```ts
repository.find({
filter: {
title: {
$in: ['春秋', '战国'],
}
}
});
```
### `$notIn`
判断字段值是否不在指定数组中。相当于 SQL 的 `NOT IN`
**示例**
```ts
repository.find({
filter: {
title: {
$notIn: ['春秋', '战国'],
}
}
});
```
### `$empty`
判断一般字段是否为空,如果是字符串字段,判断是否为空串,如果是数组字段,判断是否为空数组。
**示例**
```ts
repository.find({
filter: {
title: {
$empty: true,
}
}
});
```
### `$notEmpty`
判断一般字段是否不为空,如果是字符串字段,判断是否不为空串,如果是数组字段,判断是否不为空数组。
**示例**
```ts
repository.find({
filter: {
title: {
$notEmpty: true,
}
}
});
```
## 逻辑运算符
### `$and`
逻辑 AND。相当于 SQL 的 `AND`
**示例**
```ts
repository.find({
filter: {
$and: [
{ title: '诗经' },
{ isbn: '1234567890' },
]
}
});
```
### `$or`
逻辑 OR。相当于 SQL 的 `OR`
**示例**
```ts
repository.find({
filter: {
$or: [
{ title: '诗经' },
{ publishedAt: { $lt: '0000-00-00T00:00:00Z' } },
]
}
});
```
## 布尔类型字段运算符
用于布尔类型字段 `type: 'boolean'`
### `$isFalsy`
判断布尔类型字段值是否为假。布尔字段值为 `false`、`0` 和 `NULL` 的情况都会被判断为 `$isFalsy: true`
**示例**
```ts
repository.find({
filter: {
isPublished: {
$isFalsy: true,
}
}
})
```
### `$isTruly`
判断布尔类型字段值是否为真。布尔字段值为 `true``1` 的情况都会被判断为 `$isTruly: true`
**示例**
```ts
repository.find({
filter: {
isPublished: {
$isTruly: true,
}
}
})
```
## 数字类型字段运算符
用于数字类型字段,包括:
- `type: 'integer'`
- `type: 'float'`
- `type: 'double'`
- `type: 'real'`
- `type: 'decimal'`
### `$gt`
判断字段值是否大于指定值。相当于 SQL 的 `>`
**示例**
```ts
repository.find({
filter: {
price: {
$gt: 100,
}
}
});
```
### `$gte`
判断字段值是否大于等于指定值。相当于 SQL 的 `>=`
**示例**
```ts
repository.find({
filter: {
price: {
$gte: 100,
}
}
});
```
### `$lt`
判断字段值是否小于指定值。相当于 SQL 的 `<`
**示例**
```ts
repository.find({
filter: {
price: {
$lt: 100,
}
}
});
```
### `$lte`
判断字段值是否小于等于指定值。相当于 SQL 的 `<=`
**示例**
```ts
repository.find({
filter: {
price: {
$lte: 100,
}
}
});
```
### `$between`
判断字段值是否在指定的两个值之间。相当于 SQL 的 `BETWEEN`
**示例**
```ts
repository.find({
filter: {
price: {
$between: [100, 200],
}
}
});
```
### `$notBetween`
判断字段值是否不在指定的两个值之间。相当于 SQL 的 `NOT BETWEEN`
**示例**
```ts
repository.find({
filter: {
price: {
$notBetween: [100, 200],
}
}
});
```
## 字符串类型字段运算符
用于字符串类型字段,包括 `string`
### `$includes`
判断字符串字段是否包含指定子串。
**示例**
```ts
repository.find({
filter: {
title: {
$includes: '三字经',
}
}
})
```
### `$notIncludes`
判断字符串字段是否不包含指定子串。
**示例**
```ts
repository.find({
filter: {
title: {
$notIncludes: '三字经',
}
}
})
```
### `$startsWith`
判断字符串字段是否以指定子串开头。
**示例**
```ts
repository.find({
filter: {
title: {
$startsWith: '三字经',
}
}
})
```
### `$notStatsWith`
判断字符串字段是否不以指定子串开头。
**示例**
```ts
repository.find({
filter: {
title: {
$notStatsWith: '三字经',
}
}
})
```
### `$endsWith`
判断字符串字段是否以指定子串结尾。
**示例**
```ts
repository.find({
filter: {
title: {
$endsWith: '三字经',
}
}
})
```
### `$notEndsWith`
判断字符串字段是否不以指定子串结尾。
**示例**
```ts
repository.find({
filter: {
title: {
$notEndsWith: '三字经',
}
}
})
```
### `$like`
判断字段值是否包含指定的字符串。相当于 SQL 的 `LIKE`
**示例**
```ts
repository.find({
filter: {
title: {
$like: '计算机',
}
}
});
```
### `$notLike`
判断字段值是否不包含指定的字符串。相当于 SQL 的 `NOT LIKE`
**示例**
```ts
repository.find({
filter: {
title: {
$notLike: '计算机',
}
}
});
```
### `$iLike`
判断字段值是否包含指定的字符串,忽略大小写。相当于 SQL 的 `ILIKE`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$iLike: 'Computer',
}
}
});
```
### `$notILike`
判断字段值是否不包含指定的字符串,忽略大小写。相当于 SQL 的 `NOT ILIKE`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$notILike: 'Computer',
}
}
});
```
### `$regexp`
判断字段值是否匹配指定的正则表达式。相当于 SQL 的 `REGEXP`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$regexp: '^计算机',
}
}
});
```
### `$notRegexp`
判断字段值是否不匹配指定的正则表达式。相当于 SQL 的 `NOT REGEXP`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$notRegexp: '^计算机',
}
}
});
```
### `$iRegexp`
判断字段值是否匹配指定的正则表达式,忽略大小写。相当于 SQL 的 `~*`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$iRegexp: '^COMPUTER',
}
}
});
```
### `$notIRegexp`
判断字段值是否不匹配指定的正则表达式,忽略大小写。相当于 SQL 的 `!~*`(仅 PG 适用)。
**示例**
```ts
repository.find({
filter: {
title: {
$notIRegexp: '^COMPUTER',
}
}
});
```
## 日期类型字段运算符
用于日期类型字段 `type: 'date'`
### `$dateOn`
判断日期字段是否在某天内。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateOn: '2021-01-01',
}
}
})
```
### `$dateNotOn`
判断日期字段是否不在某天内。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateNotOn: '2021-01-01',
}
}
})
```
### `$dateBefore`
判断日期字段是否在某个值之前。相当于小于传入的日期值。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateBefore: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateNotBefore`
判断日期字段是否不在某个值之前。相当于大于等于传入的日期值。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateNotBefore: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateAfter`
判断日期字段是否在某个值之后。相当于大于传入的日期值。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateAfter: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateNotAfter`
判断日期字段是否不在某个值之后。相当于小于等于传入的日期值。
**示例**
```ts
repository.find({
filter: {
createdAt: {
$dateNotAfter: '2021-01-01T00:00:00.000Z',
}
}
})
```
## 数组类型字段运算符
用于数组类型字段 `type: 'array'`
### `$match`
判断数组字段的值是否匹配指定数组中的值。
**示例**
```ts
repository.find({
filter: {
tags: {
$match: ['文学', '历史'],
}
}
})
```
### `$notMatch`
判断数组字段的值是否不匹配指定数组中的值。
**示例**
```ts
repository.find({
filter: {
tags: {
$notMatch: ['文学', '历史'],
}
}
})
```
### `$anyOf`
判断数组字段的值是否包含指定数组中的任意值。
**示例**
```ts
repository.find({
filter: {
tags: {
$anyOf: ['文学', '历史'],
}
}
})
```
### `$noneOf`
判断数组字段的值是否不包含指定数组中的任意值。
**示例**
```ts
repository.find({
filter: {
tags: {
$noneOf: ['文学', '历史'],
}
}
})
```
### `$arrayEmpty`
判断数组字段是否为空。
**示例**
```ts
repository.find({
filter: {
tags: {
$arrayEmpty: true,
}
}
});
```
### `$arrayNotEmpty`
判断数组字段是否不为空。
**示例**
```ts
repository.find({
filter: {
tags: {
$arrayNotEmpty: true,
}
}
});
```
## 关系字段类型运算符
用于判断关系是否存在,字段类型包括:
- `type: 'hasOne'`
- `type: 'hasMany'`
- `type: 'belongsTo'`
- `type: 'belongsToMany'`
### `$exists`
有关系数据
**示例**
```ts
repository.find({
filter: {
author: {
$exists: true,
}
}
});
```
### `$notExists`
无关系数据
**示例**
```ts
repository.find({
filter: {
author: {
$notExists: true,
}
}
});
```

View File

@ -0,0 +1,181 @@
# BelongsToManyRepository
`BelongsToManyRepository` 是用于处理 `BelongsToMany` 关系的 `Relation Repository`
不同于其他关系类型,`BelongsToMany` 类型的关系需要通过中间表来记录。
`Nocobase` 中定义关联关系,可自动创建中间表,也可以明确指定中间表。
## 类方法
### `find()`
查找关联对象
**签名**
* `async find(options?: FindOptions): Promise<M[]>`
**详细信息**
查询参数与 [`Repository.find()`](../repository.md#find) 一致。
### `findOne()`
查找关联对象,仅返回一条记录
**签名**
* `async findOne(options?: FindOneOptions): Promise<M>`
<embed src="../shared/find-one.md"></embed>
### `count()`
返回符合查询条件的记录数
**签名**
* `async count(options?: CountOptions)`
**类型**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
### `findAndCount()`
从数据库查询特定条件的数据集和结果数。
**签名**
* `async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>`
**类型**
```typescript
type FindAndCountOptions = CommonFindOptions
```
### `create()`
创建关联对象
**签名**
* `async create(options?: CreateOptions): Promise<M>`
<embed src="../shared/create-options.md"></embed>
### `update()`
更新符合条件的关联对象
**签名**
* `async update(options?: UpdateOptions): Promise<M>`
<embed src="../shared/update-options.md"></embed>
### `destroy()`
删除符合条件的关联对象
**签名**
* `async destroy(options?: TargetKey | TargetKey[] | DestroyOptions): Promise<Boolean>`
<embed src="../shared/destroy-options.md"></embed>
### `add()`
添加新的关联对象
**签名**
* `async add(
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions
): Promise<void>`
**类型**
```typescript
type PrimaryKeyWithThroughValues = [TargetKey, Values];
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[];
}
```
**详细信息**
可以直接传入关联对象的 `targetKey`,也可将 `targetKey` 与中间表的字段值一并传入。
**示例**
```typescript
const t1 = await Tag.repository.create({
values: { name: 't1' },
});
const t2 = await Tag.repository.create({
values: { name: 't2' },
});
const p1 = await Post.repository.create({
values: { title: 'p1' },
});
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
// 传入 targetKey
PostTagRepository.add([
t1.id, t2.id
]);
// 传入中间表字段
PostTagRepository.add([
[t1.id, { tagged_at: '123' }],
[t2.id, { tagged_at: '456' }],
]);
```
### `set()`
设置关联对象
**签名**
* async set(
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions,
): Promise<void>
**详细信息**
参数同 [add()](#add)
### `remove()`
移除与给定对象之间的关联关系
**签名**
* `async remove(options: TargetKey | TargetKey[] | AssociatedOptions)`
**类型**
```typescript
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[];
}
```
### `toggle()`
切换关联对象。
在一些业务场景中,经常需要切换关联对象,比如用户收藏商品,用户可以取消收藏,也可以再次收藏。使用 `toggle` 方法可以快速实现类似功能。
**签名**
* `async toggle(options: TargetKey | { tk?: TargetKey; transaction?: Transaction }): Promise<void>`
**详细信息**
`toggle` 方法会自动判断关联对象是否已经存在,如果存在则移除,如果不存在则添加。

View File

@ -0,0 +1,3 @@
## BelongsToRepository
`BelongsToRepository` 是用于处理 `BelongsTo` 关系的 `Repository`,它提供了一些便捷的方法来处理 `BelongsTo` 关系。其接口与 [HasOneRepository](./has-one-repository.md) 一致。

View File

@ -0,0 +1,132 @@
# HasManyRepository
`HasManyRepository` 是用于处理 `HasMany` 关系的 `Relation Repository`
## 类方法
### `find()`
查找关联对象
**签名**
* `async find(options?: FindOptions): Promise<M[]>`
**详细信息**
查询参数与 [`Repository.find()`](../repository.md#find) 一致。
### `findOne()`
查找关联对象,仅返回一条记录
**签名**
* `async findOne(options?: FindOneOptions): Promise<M>`
<embed src="../shared/find-one.md"></embed>
### `count()`
返回符合查询条件的记录数
**签名**
* `async count(options?: CountOptions)`
**类型**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
### `findAndCount()`
从数据库查询特定条件的数据集和结果数。
**签名**
* `async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>`
**类型**
```typescript
type FindAndCountOptions = CommonFindOptions
```
### `create()`
创建关联对象
**签名**
* `async create(options?: CreateOptions): Promise<M>`
<embed src="../shared/create-options.md"></embed>
### `update()`
更新符合条件的关联对象
**签名**
* `async update(options?: UpdateOptions): Promise<M>`
<embed src="../shared/update-options.md"></embed>
### `destroy()`
删除符合条件的关联对象
**签名**
* `async destroy(options?: TK | DestroyOptions): Promise<M>`
<embed src="../shared/destroy-options.md"></embed>
### `add()`
添加对象关联关系
**签名**
* `async add(options: TargetKey | TargetKey[] | AssociatedOptions)`
**类型**
```typescript
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[];
}
```
**详细信息**
* `tk` - 关联对象的 targetKey 值,可以是单个值,也可以是数组。
<embed src="../shared/transaction.md"></embed>
### `remove()`
移除与给定对象之间的关联关系
**签名**
* `async remove(options: TargetKey | TargetKey[] | AssociatedOptions)`
**详细信息**
参数同 [`add()`](#add) 方法。
### `set()`
设置当前关系的关联对象
**签名**
* `async set(options: TargetKey | TargetKey[] | AssociatedOptions)`
**详细信息**
参数同 [`add()`](#add) 方法。

View File

@ -0,0 +1,181 @@
# HasOneRepository
`HasOneRepository``HasOne` 类型的关联 Repository。
```typescript
const User = db.collection({
name: 'users',
fields: [
{ type: 'hasOne', name: 'profile' },
{ type: 'string', name: 'name' },
],
});
const Profile = db.collection({
name: 'profiles',
fields: [{ type: 'string', name: 'avatar' }],
});
const user = await User.repository.create({
values: { name: 'u1' },
});
// 创建 HasOneRepository 实例
const userProfileRepository = new HasOneRepository(User, 'profile', user.get('id'));
```
## 类方法
### `find()`
查找关联对象
**签名**
* `async find(options?: SingleRelationFindOption): Promise<Model<any> | null>`
**类型**
```typescript
interface SingleRelationFindOption extends Transactionable {
fields?: Fields;
except?: Except;
appends?: Appends;
filter?: Filter;
}
```
**详细信息**
查询参数与 [`Repository.find()`](../repository.md#find) 一致。
**示例**
```typescript
const profile = await UserProfileRepository.find();
// 关联对象不存在时,返回 null
```
### `create()`
创建关联对象
**签名**
* `async create(options?: CreateOptions): Promise<Model>`
<embed src="../shared/create-options.md"></embed>
**示例**
```typescript
const profile = await UserProfileRepository.create({
values: { avatar: 'avatar1' },
});
console.log(profile.toJSON());
/*
{
id: 1,
avatar: 'avatar1',
userId: 1,
updatedAt: 2022-09-24T13:59:40.025Z,
createdAt: 2022-09-24T13:59:40.025Z
}
*/
```
### `update()`
更新关联对象
**签名**
* `async update(options: UpdateOptions): Promise<Model>`
<embed src="../shared/update-options.md"></embed>
**示例**
```typescript
const profile = await UserProfileRepository.update({
values: { avatar: 'avatar2' },
});
profile.get('avatar'); // 'avatar2'
```
### `remove()`
移除关联对象,仅解除关联关系,不删除关联对象
**签名**
* `async remove(options?: Transactionable): Promise<void>`
**详细信息**
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```typescript
await UserProfileRepository.remove();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 1; // true
```
### `destroy()`
删除关联对象
**签名**
* `async destroy(options?: Transactionable): Promise<Boolean>`
**详细信息**
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```typescript
await UserProfileRepository.destroy();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 0; // true
```
### `set()`
设置关联对象
**签名**
* `async set(options: TargetKey | SetOption): Promise<void>`
**类型**
```typescript
interface SetOption extends Transactionable {
tk?: TargetKey;
}
````
**详细信息**
* tk: 设置关联对象的 targetKey
* transaction: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```typescript
const newProfile = await Profile.repository.create({
values: { avatar: 'avatar2' },
});
await UserProfileRepository.set(newProfile.get('id'));
(await UserProfileRepository.find()).get('id') === newProfile.get('id'); // true
```

View File

@ -0,0 +1,45 @@
# RelationRepository
`RelationRepository` 是关系类型的 `Repository` 对象,`RelationRepository` 可以实现在不加载关联的情况下对关联数据进行操作。基于 `RelationRepository`,每种关联都派生出对应的实现,分别为
* [`HasOneRepository`](#has-one-repository)
* `HasManyRepository`
* `BelongsToRepository`
* `BelongsToManyRepository`
## 构造函数
**签名**
* `constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `sourceCollection` | `Collection` | - | 关联中的参照关系referencing relation对应的 Collection |
| `association` | `string` | - | 关联名称 |
| `sourceKeyValue` | `string \| number` | - | 参照关系中对应的 key 值 |
## 基类属性
### `db: Database`
数据库对象
### `sourceCollection`
关联中的参照关系referencing relation对应的 Collection
### `targetCollection`
关联中被参照关系referenced relation对应的 Collection
### `association`
sequelize 中的与当前关联对应的 association 对象
### `associationField`
collection 中的与当前关联对应的字段
### `sourceKeyValue`
参照关系中对应的 key 值

View File

@ -0,0 +1,352 @@
# Repository
数据表数据仓库管理类。大部分基于数据表的数据存取等操作均通过该类实现。
## 构造函数
通常不会直接由开发者调用,主要通过 `db.registerRepositories()` 注册类型以后,在 `db.colletion()` 的参数中指定对应已注册的仓库类型,并完成实例化。
**签名**
* `constructor(collection: Collection)`
**示例**
```ts
import { Repository } from '@nocobase/database';
class MyRepository extends Repository {
async myQuery(sql) {
return this.database.sequelize.query(sql);
}
}
db.registerRepositories({
books: MyRepository
});
db.collection({
name: 'books',
// here link to the registered repository
repository: 'books'
});
await db.sync();
const books = db.getRepository('books') as MyRepository;
await books.myQuery('SELECT * FROM books;');
```
## 实例成员
### `database`
上下文所在的数据库管理实例。
### `collection`
对应的数据表管理实例。
### `model`
对应的数据模型类。
## 实例方法
### `find()`
从数据库查询特定条件的数据集。相当于 Sequelize 中的 `Model.findAll()`
**签名**
* `async find(options?: FindOptions): Promise<Model[]>`
**类型**
```typescript
type Filter = FilterWithOperator | FilterWithValue | FilterAnd | FilterOr;
type Appends = string[];
type Except = string[];
type Fields = string[];
type Sort = string[] | string;
interface SequelizeFindOptions {
limit?: number;
offset?: number;
}
interface FilterByTk {
filterByTk?: TargetKey;
}
interface CommonFindOptions extends Transactionable {
filter?: Filter;
fields?: Fields;
appends?: Appends;
except?: Except;
sort?: Sort;
}
type FindOptions = SequelizeFindOptions & CommonFindOptions & FilterByTk;
```
**详细信息**
#### `filter: Filter`
查询条件,用于过滤数据结果。传入的查询参数中,`key` 为查询的字段名,`value` 可传要查询的值,
也可配合使用操作符进行其他条件的数据筛选。
```typescript
// 查询 name 为 foo并且 age 大于 18 的记录
repository.find({
filter: {
name: "foo",
age: {
$gt: 18,
},
}
})
```
更多操作符请参考 [查询操作符](./operators.md)。
#### `filterByTk: TargetKey`
通过 `TargetKey` 查询数据,为 `filter` 参数的便捷方法。`TargetKey` 具体是哪一个字段,
可在 `Collection` 中进行[配置](./collection.md#filtertargetkey),默认为 `primaryKey`
```typescript
// 默认情况下,查找 id 为 1 的记录
repository.find({
filterByTk: 1,
});
```
#### `fields: string[]`
查询列,用户控制数据字段结果。传入此参数之后,只会返回指定的字段。
#### `except: string[]`
排除列,用于控制数据字段结果。传入此参数之后,传入的字段将不会输出。
#### `appends: string[]`
追加列,用于加载关联数据。传入此参数之后,指定的关联字段将一并输出。
#### `sort: string[] | string`
指定查询结果排序方式,传入参数为字段名称,默认按照升序 `asc` 排序,若需按降序 `desc` 排序,
可在字段名称前加上 `-` 符号,如:`['-id', 'name']`,表示按 `id desc, name asc` 排序。
#### `limit: number`
限制结果数量,同 `SQL` 中的 `limit`
#### `offset: number`
查询偏移量,同 `SQL` 中的 `offset`
**示例**
```ts
const posts = db.getRepository('posts');
const results = await posts.find({
filter: {
createdAt: {
$gt: '2022-01-01T00:00:00.000Z',
}
},
fields: ['title'],
appends: ['user'],
});
```
### `findOne()`
从数据库查询特定条件的单条数据。相当于 Sequelize 中的 `Model.findOne()`
**签名**
* `async findOne(options?: FindOneOptions): Promise<Model | null>`
<embed src="./shared/find-one.md"></embed>
**示例**
```ts
const posts = db.getRepository('posts');
const result = await posts.findOne({
filterByTk: 1,
});
```
### `count()`
从数据库查询特定条件的数据总数。相当于 Sequelize 中的 `Model.count()`
**签名**
* `count(options?: CountOptions): Promise<number>`
**类型**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
**示例**
```ts
const books = db.getRepository('books');
const count = await books.count({
filter: {
title: '三字经'
}
});
```
### `findAndCount()`
从数据库查询特定条件的数据集和结果数。相当于 Sequelize 中的 `Model.findAndCountAll()`
**签名**
* `async findAndCount(options?: FindAndCountOptions): Promise<[Model[], number]>`
**类型**
```typescript
type FindAndCountOptions = Omit<SequelizeAndCountOptions, 'where' | 'include' | 'order'> & CommonFindOptions;
```
**详细信息**
查询参数与 `find()` 相同。返回值为一个数组,第一个元素为查询结果,第二个元素为结果总数。
### `create()`
向数据表插入一条新创建的数据。相当于 Sequelize 中的 `Model.create()`。当要创建的数据对象携带关系字段的信息时,会一并创建或更新相应的关系数据记录。
**签名**
* `async create<M extends Model>(options: CreateOptions): Promise<M>`
<embed src="./shared/create-options.md"></embed>
**示例**
```ts
const posts = db.getRepository('posts');
const result = await posts.create({
values: {
title: 'NocoBase 1.0 发布日志',
tags: [
// 有关系表主键值时为更新该条数据
{ id: 1 },
// 没有主键值时为创建新数据
{ name: 'NocoBase' },
]
},
});
```
### `createMany()`
向数据表插入多条新创建的数据。相当于多次调用 `create()` 方法。
**签名**
* `createMany(options: CreateManyOptions): Promise<Model[]>`
**类型**
```typescript
interface CreateManyOptions extends BulkCreateOptions {
records: Values[];
}
```
**详细信息**
* `records`:要创建的记录的数据对象数组。
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```ts
const posts = db.getRepository('posts');
const results = await posts.createMany({
records: [
{
title: 'NocoBase 1.0 发布日志',
tags: [
// 有关系表主键值时为更新该条数据
{ id: 1 },
// 没有主键值时为创建新数据
{ name: 'NocoBase' },
]
},
{
title: 'NocoBase 1.1 发布日志',
tags: [
{ id: 1 }
]
},
],
});
```
### `update()`
更新数据表中的数据。相当于 Sequelize 中的 `Model.update()`。当要更新的数据对象携带关系字段的信息时,会一并创建或更新相应的关系数据记录。
**签名**
* `async update<M extends Model>(options: UpdateOptions): Promise<M>`
<embed src="./shared/update-options.md"></embed>
**示例**
```ts
const posts = db.getRepository('posts');
const result = await posts.update({
filterByTk: 1,
values: {
title: 'NocoBase 1.0 发布日志',
tags: [
// 有关系表主键值时为更新该条数据
{ id: 1 },
// 没有主键值时为创建新数据
{ name: 'NocoBase' },
]
},
});
```
### `destory()`
删除数据表中的数据。相当于 Sequelize 中的 `Model.destroy()`
**签名**
* `async destory(options?: TargetKey | TargetKey[] | DestoryOptions): Promise<number>`
**类型**
```typescript
interface DestroyOptions extends SequelizeDestroyOptions {
filter?: Filter;
filterByTk?: TargetKey | TargetKey[];
truncate?: boolean;
context?: any;
}
```
**详细信息**
* `filter`指定要删除的记录的过滤条件。Filter 详细用法可参考 [`find()`](#find) 方法。
* `filterByTk`:按 TargetKey 指定要删除的记录的过滤条件。
* `truncate`: 是否清空表数据,在没有传入 `filter``filterByTk` 参数时有效。
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。

View File

@ -0,0 +1,8 @@
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.values` | `M` | `{}` | 插入的数据对象 |
| `options.whitelist?` | `string[]` | - | `values` 字段的白名单,只有名单内的字段会被存储 |
| `options.blacklist?` | `string[]` | - | `values` 字段的黑名单,名单内的字段不会被存储 |
| `options.transaction?` | `Transaction` | - | 事务 |

View File

@ -0,0 +1,21 @@
**类型**
```typescript
type WhiteList = string[];
type BlackList = string[];
type AssociationKeysToBeUpdate = string[];
interface CreateOptions extends SequelizeCreateOptions {
values?: Values;
whitelist?: WhiteList;
blacklist?: BlackList;
updateAssociationValues?: AssociationKeysToBeUpdate;
context?: any;
}
```
**详细信息**
* `values`:要创建的记录的数据对象。
* `whitelist`:指定要创建的记录的数据对象中,哪些字段**可以被写入**。若不传入此参数,则默认允许所有字段写入。
* `blacklist`:指定要创建的记录的数据对象中,哪些字段**不允许被写入**。若不传入此参数,则默认允许所有字段写入。
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。

View File

@ -0,0 +1,17 @@
**类型**
```typescript
interface DestroyOptions extends SequelizeDestroyOptions {
filter?: Filter;
filterByTk?: TargetKey | TargetKey[];
truncate?: boolean;
context?: any;
}
```
**详细信息**
* `filter`指定要删除的记录的过滤条件。Filter 详细用法可参考 [`find()`](#find) 方法。
* `filterByTk`:按 TargetKey 指定要删除的记录的过滤条件。
* `truncate`: 是否清空表数据,在没有传入 `filter``filterByTk` 参数时有效。
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。

View File

@ -0,0 +1,8 @@
**类型**
```typescript
type FindOneOptions = Omit<FindOptions, 'limit'>;
```
**参数**
大部分参数与 `find()` 相同,不同之处在于 `findOne()` 只返回单条数据,所以不需要 `limit` 参数,且查询时始终为 `1`

View File

@ -0,0 +1 @@
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。

View File

@ -0,0 +1,24 @@
**类型**
```typescript
interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> {
values: Values;
filter?: Filter;
filterByTk?: TargetKey;
whitelist?: WhiteList;
blacklist?: BlackList;
updateAssociationValues?: AssociationKeysToBeUpdate;
context?: any;
}
```
**详细信息**
* `values`:要更新的记录的数据对象。
* `filter`:指定要更新的记录的过滤条件, Filter 详细用法可参考 [`find()`](#find) 方法。
* `filterByTk`:按 TargetKey 指定要更新的记录的过滤条件。
* `whitelist`: `values` 字段的白名单,只有名单内的字段会被写入。
* `blacklist`: `values` 字段的黑名单,名单内的字段不会被写入。
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
`filterByTk``filter` 至少要传其一。

225
docs/en-US/api/env.md Normal file
View File

@ -0,0 +1,225 @@
# 环境变量
## 全局环境变量
保存在 `.env` 文件里
### APP_ENV
应用环境,默认值 `development`,可选项包括:
- `production` 生产环境
- `development` 开发环境
```bash
APP_ENV=production
```
### APP_HOST
应用主机,默认值 `0.0.0.0`
```bash
APP_HOST=192.168.3.154
```
### APP_PORT
应用端口,默认值 `13000`
```bash
APP_PORT=13000
```
### APP_KEY
秘钥,用于 jwt 等场景
```bash
APP_KEY=app-key-test
```
### API_BASE_PATH
NocoBase API 地址前缀,默认值 `/api/`
```bash
API_BASE_PATH=/api/
```
### PLUGIN_PACKAGE_PREFIX
插件包前缀,默认值 `@nocobase/plugin-,@nocobase/preset-`
例如,有一名为 `my-nocobase-app` 的项目,新增了 `hello` 插件,包名为 `@my-nocobase-app/plugin-hello`
PLUGIN_PACKAGE_PREFIX 配置如下:
```bash
PLUGIN_PACKAGE_PREFIX=@nocobase/plugin-,@nocobase/preset-,@my-nocobase-app/plugin-
```
插件名和包名的对应关系为:
- `users` 插件包名为 `@nocobase/plugin-users`
- `nocobase` 插件包名为 `@nocobase/preset-nocobase`
- `hello` 插件包名为 `@my-nocobase-app/plugin-hello`
### DB_DIALECT
数据库类型,默认值 `sqlite`,可选项包括:
- `sqlite`
- `mysql`
- `postgres`
```bash
DB_DIALECT=mysql
```
### DB_STORAGE
数据库文件路径(使用 SQLite 数据库时配置)
```bash
# 相对路径
DB_HOST=storage/db/nocobase.db
# 绝对路径
DB_HOST=/your/path/nocobase.db
```
### DB_HOST
数据库主机(使用 mysql 或 postgres 数据库时需要配置)
默认值 `localhost`
```bash
DB_HOST=localhost
```
### DB_PORT
数据库端口(使用 mysql 或 postgres 数据库时需要配置)
- MySQL 默认端口 3306
- PostgreSQL 默认端口 5432
```bash
DB_PORT=3306
```
### DB_DATABASE
数据库名(使用 mysql 或 postgres 数据库时需要配置)
```bash
DB_DATABASE=nocobase
```
### DB_USER
数据库用户(使用 mysql 或 postgres 数据库时需要配置)
```bash
DB_USER=nocobase
```
### DB_PASSWORD
数据库密码(使用 mysql 或 postgres 数据库时需要配置)
```bash
DB_PASSWORD=nocobase
```
### DB_TABLE_PREFIX
数据表前缀
```bash
DB_TABLE_PREFIX=nocobase_
```
### DB_LOGGING
数据库日志开关,默认值 `off`,可选项包括:
- `on` 打开
- `off` 关闭
```bash
DB_LOGGING=on
```
## 临时环境变量
安装 NocoBase 时,可以通过设置临时的环境变量来辅助安装,如:
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
# 等同于
yarn nocobase install \
--lang=zh-CN \
--root-email=demo@nocobase.com \
--root-password=admin123 \
--root-nickname="Super Admin"
# 等同于
yarn nocobase install -l zh-CN -e demo@nocobase.com -p admin123 -n "Super Admin"
```
### INIT_APP_LANG
安装时的语言,默认值 `en-US`,可选项包括:
- `en-US`
- `zh-CN`
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
nocobase install
```
### INIT_ROOT_EMAIL
Root 用户邮箱
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
nocobase install
```
### INIT_ROOT_PASSWORD
Root 用户密码
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
nocobase install
```
### INIT_ROOT_NICKNAME
Root 用户昵称
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
```

12
docs/en-US/api/index.md Normal file
View File

@ -0,0 +1,12 @@
# 概览
| 模块 | 包名 | 描述 |
| --------------------------------- | --------------------- | ------------------- |
| [Server](/api/server) | `@nocobase/server` | 服务端应用 |
| [Database](/api/database) | `@nocobase/database` | 数据库访问层 |
| [Resourcer](/api/resourcer) | `@nocobase/resourcer` | 资源与路由映射 |
| [ACL](/api/acl) | `@nocobase/acl` | 访问控制表 |
| [Client](/api/client/application) | `@nocobase/client` | 客户端应用 |
| [CLI](/api/cli) | `@nocobase/cli` | NocoBase 命令行工具 |
| [SDK](/api/sdk) | `@nocobase/sdk` | NocoBase SDK |
| [Actions](/api/actions) | `@nocobase/actions` | 内置常用资源操作 |

View File

@ -0,0 +1,148 @@
# Action
Action 是对资源的操作过程的描述,通常包含数据库处理等,类似其他框架中的 service 层,最简化的实现可以是一个 Koa 的中间件函数。在资源管理器里,针对特定资源定义的普通操作函数会被包装成 Action 类型的实例,当请求匹配对应资源的操作时,执行对应的操作过程。
## 构造函数
通常不需要直接实例化 Action而是由资源管理器自动调用 `Action` 的静态方法 `toInstanceMap()` 进行实例化。
### `constructor(options: ActionOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `handler` | `Function` | - | 操作函数 |
| `middlewares?` | `Middleware \| Middleware[]` | - | 针对操作的中间件 |
| `values?` | `Object` | - | 默认的操作数据 |
| `fields?` | `string[]` | - | 默认针对的字段组 |
| `appends?` | `string[]` | - | 默认附加的关联字段组 |
| `except?` | `string[]` | - | 默认排除的字段组 |
| `filter` | `FilterOptions` | - | 默认的过滤参数 |
| `sort` | `string[]` | - | 默认的排序参数 |
| `page` | `number` | - | 默认的页码 |
| `pageSize` | `number` | - | 默认的每页数量 |
| `maxPageSize` | `number` | - | 默认最大每页数量 |
## 实例成员
### `actionName`
被实例化后对应的操作名称。在实例化时从请求中解析获取。
### `resourceName`
被实例化后对应的资源名称。在实例化时从请求中解析获取。
### `resourceOf`
被实例化后对应的关系资源的主键值。在实例化时从请求中解析获取。
### `readonly middlewares`
针对操作的中间件列表。
### `params`
操作参数。包含对应操作的所有相关参数,实例化时根据定义的 action 参数初始化,之后请求中解析前端传入的参数并根据对应参数的合并策略合并。如果有其他中间件的处理,也会有类似的合并过程。直到 handler 处理时,访问 params 得到的是经过多次合并的最终参数。
参数的合并过程提供了针对操作处理的可扩展性,可以通过自定义中间件的方式按业务需求进行参数的前置解析和处理,例如表单提交的参数验证就可以在此环节实现。
预设的参数可以参考 [/api/actions] 中不同操作的参数。
参数中还包含请求资源路由的描述部分,具体如下:
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `resourceName` | `string` | - | 资源名称 |
| `resourceIndex` | `string \| number` | - | 资源的主键值 |
| `associatedName` | `string` | - | 所属关系资源的名称 |
| `associatedIndex` | `string \| number` | - | 所属关系资源的主键值 |
| `associated` | `Object` | - | 所属关系资源的实例 |
| `actionName` | `string` | - | 操作名称 |
**示例**
```ts
app.resourcer.define('books', {
actions: {
publish(ctx, next) {
ctx.body = ctx.action.params.values;
// {
// id: 1234567890
// publishedAt: '2019-01-01',
// }
}
},
middlewares: [
async (ctx, next) => {
ctx.action.mergeParams({
values: {
id: Math.random().toString(36).substr(2, 10),
publishedAt: new Date(),
}
});
await next();
}
]
});
```
## 实例方法
### `mergeParams()`
将额外的参数合并至当前参数集,且可以根据不同的策略进行合并。
**签名**
* `mergeParams(params: ActionParams, strategies: MergeStrategies = {})`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `params` | `ActionParams` | - | 额外的参数集 |
| `strategies` | `MergeStrategies` | - | 针对每个参数的合并策略 |
内置操作的默认合并策略如下表:
| 参数名 | 类型 | 默认值 | 合并策略 | 描述 |
| --- | --- | --- | --- | --- |
| `filterByTk` | `number \| string` | - | SQL `and` | 查询主键值 |
| `filter` | `FilterOptions` | - | SQL `and` | 查询过滤参数 |
| `fields` | `string[]` | - | 取并集 | 字段组 |
| `appends` | `string[]` | `[]` | 取并集 | 附加的关联字段组 |
| `except` | `string[]` | `[]` | 取并集 | 排除的字段组 |
| `whitelist` | `string[]` | `[]` | 取交集 | 可处理字段的白名单 |
| `blacklist` | `string[]` | `[]` | 取并集 | 可处理字段的黑名单 |
| `sort` | `string[]` | - | SQL `order by` | 查询排序参数 |
| `page` | `number` | - | 覆盖 | 页码 |
| `pageSize` | `number` | - | 覆盖 | 每页数量 |
| `values` | `Object` | - | 深度合并 | 操作提交的数据 |
**示例**
```ts
ctx.action.mergeParams({
filter: {
name: 'foo',
},
fields: ['id', 'name'],
except: ['name'],
sort: ['id'],
page: 1,
pageSize: 10,
values: {
name: 'foo',
},
}, {
filter: 'and',
fields: 'union',
except: 'union',
sort: 'overwrite',
page: 'overwrite',
pageSize: 'overwrite',
values: 'deepMerge',
});
```

View File

@ -0,0 +1,252 @@
# Resourcer
Resourcer 主要用于管理 API 资源与路由,也是 NocoBase 的内置模块app 默认会自动创建一个 Resourcer 实例,大部分情况你可以通过 `app.resourcer` 访问。
资源路由管理器主要通过 [资源](/api/server/resourcer/resource) + [操作](/api/server/resourcer/action) 的概念定义服务端 API 接口,与 RESTful 的概念相似。大部分资源通过映射数据库表生成,包含常规的 CRUD 操作,以覆盖常见场景。但如果有额外需求,也可以在此基础上扩展更多的资源类型和操作类型。
## 包结构
可通过以下方式引入相关实体:
```ts
import Resourcer, {
Resource,
Action,
Middleware,
branch
} from '@nocobase/resourcer';
```
## 构造函数
用于创建 Resourcer 管理器实例。由于 app 默认创建一个内置实例,所以通常不会直接使用构造函数。
**签名**
* `constructor(options: ResourcerOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `prefix` | `string` | - | 路由路径前缀 |
| `accessors` | `Object` | _以下成员值_ | 默认操作方法名称标识 |
| `accessors.list` | `string` | `'list'` | 列举操作方法名称标识 |
| `accessors.get` | `string` | `'get'` | 获取操作方法名称标识 |
| `accessors.create` | `string` | `'create'` | 创建操作方法名称标识 |
| `accessors.update` | `string` | `'update'` | 更新操作方法名称标识 |
| `accessors.delete` | `string` | `'destroy'` | 删除操作方法名称标识 |
| `accessors.add` | `string` | `'add'` | 增加关联操作方法名称标识 |
| `accessors.remove` | `string` | `'remove'` | 移除关联操作方法名称标识 |
| `accessors.set` | `string` | `'set'` | 全量设置关联操作方法名称标识 |
**示例**
在创建 app 时件时,可以通过 `resourcer` 选项传入:
```ts
const app = new Application({
// 对应默认 resourcer 实例的配置项
resourcer: {
prefix: process.env.API_BASE_PATH
}
});
```
## 实例方法
### `define()`
定义并向资源管理器注册一个资源对象。通常代替 `Resource` 类的构造函数使用。
**签名**
* `define(options: ResourceOptions): Resource`
**参数**
详见 [Resource 构造函数](/api/server/resourcer/resource#构造函数)。
**示例**
```ts
app.resourcer.define({
name: 'books',
actions: {
// 扩展的 action
publish(ctx, next) {
ctx.body = 'ok';
}
}
});
```
### `isDefined()`
检查对应名称的资源是否已被注册。
**签名**
* `isDefined(name: string): boolean`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 资源名称 |
**示例**
```ts
app.resourcer.isDefined('books'); // true
```
### `registerAction()`
向资源管理器注册一个操作,可以指定针对特定的资源,如不指定资源名称,则认为是针对全局所有资源都可访问的操作。
**签名**
* `registerAction(name: string, handler: HandlerType): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 操作名称 |
| `handler` | `HandlerType` | - | 操作处理函数 |
`name` 的值如果以 `<resourceName>:` 开头则代表仅针对 `<resourceName>` 资源可访问,否则认为是全局操作。
**示例**
```ts
// 注册后任意资源都可以进行 upload 操作
app.resourcer.registerAction('upload', async (ctx, next) => {
ctx.body = 'ok';
});
// 仅针对 attachments 资源注册 upload 操作
app.resourcer.registerAction('attachments:upload', async (ctx, next) => {
ctx.body = 'ok';
});
```
### `registerActions()`
向资源管理器注册多个操作的集合方法。
**签名**
* `registerActions(actions: { [name: string]: HandlerType }): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `actions` | `{ [name: string]: HandlerType }` | - | 操作集合 |
**示例**
```ts
app.resourcer.registerActions({
upload: async (ctx, next) => {
ctx.body = 'ok';
},
'attachments:upload': async (ctx, next) => {
ctx.body = 'ok';
}
});
```
### `getResource()`
获取对应名称的资源对象。
**签名**
* `getResource(name: string): Resource`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 资源名称 |
**示例**
```ts
app.resourcer.getResource('books');
```
### `getAction()`
获取对应名称的操作处理函数。
**签名**
* `getAction(name: string): HandlerType`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 操作名称 |
`name` 的值如果以 `<resourceName>:` 开头则代表仅针对 `<resourceName>` 资源的操作,否则认为是全局操作。
**示例**
```ts
app.resourcer.getAction('upload');
app.resourcer.getAction('attachments:upload');
```
### `use()`
以 Koa 的形式注册一个中间件,中间件形成一个队列,并排在所有资源的操作处理函数之前执行。
**签名**
* `use(middleware: Middleware | Middleware[]): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `middleware` | `Middleware \| Middleware[]` | - | 中间件 |
**示例**
```ts
app.resourcer.use(async (ctx, next) => {
console.log(ctx.req.url);
await next();
});
```
### `middleware()`
生成一个兼容 Koa 的中间件,用于将资源的路由处理注入到应用中。
**签名**
* `middleware(options: KoaMiddlewareOptions): KoaMiddleware`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.prefix?` | `string` | `''` | 路径前缀。 |
| `options.accessors?` | `Object` | `{}` | 常用方法的名称映射,与构造函数的 `accessors` 参数结构相同。 |
**示例**
```ts
const koa = new Koa();
const resourcer = new Resourcer();
// 生成兼容 Koa 的中间件
koa.use(resourcer.middleware());
```

View File

@ -0,0 +1,168 @@
# Middleware
与 Koa 的中间件类似,但提供了更多增强的功能,可以方便的进行更多的扩展。
中间件定义后可以在资源管理器等多处进行插入使用,由开发者自行控制调用的时机。
## 构造函数
**签名**
* `constructor(options: Function)`
* `constructor(options: MiddlewareOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `Function` | - | 中间件处理函数 |
| `options` | `MiddlewareOptions ` | - | 中间件配置项 |
| `options.only` | `string[]` | - | 仅允许指定的操作 |
| `options.except` | `string[]` | - | 排除指定的操作 |
| `options.handler` | `Function` | - | 处理函数 |
**示例**
简单定义:
```ts
const middleware = new Middleware((ctx, next) => {
await next();
});
```
使用相关参数:
```ts
const middleware = new Middleware({
only: ['create', 'update'],
async handler(ctx, next) {
await next();
},
});
```
## 实例方法
### `getHandler()`
返回已经过编排的处理函数。
**示例**
以下中间件在请求时会先输出 `1`,再输出 `2`
```ts
const middleware = new Middleware((ctx, next) => {
console.log(1);
await next();
});
middleware.use(async (ctx, next) => {
console.log(2);
await next();
});
app.resourcer.use(middleware.getHandler());
```
### `use()`
对当前中间件添加中间件函数。用于提供中间件的扩展点。示例见 `getHandler()`
**签名**
* `use(middleware: Function)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `middleware` | `Function` | - | 中间件处理函数 |
### `disuse()`
移除当前中间件已添加的中间件函数。
**签名**
* `disuse(middleware: Function)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `middleware` | `Function` | - | 中间件处理函数 |
**示例**
以下示例在请求处理是只输出 `1`,不执行 fn1 中的 `2` 输出。
```ts
const middleware = new Middleware((ctx, next) => {
console.log(1);
await next();
});
async function fn1(ctx, next) {
console.log(2);
await next();
}
middleware.use(fn1);
app.resourcer.use(middleware.getHandler());
middleware.disuse(fn1);
```
### `canAccess()`
判断当前中间件针对特定操作是否要被调用,通常由资源管理器内部处理。
**签名**
* `canAccess(name: string): boolean`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 操作名称 |
## 其他导出
### `branch()`
创建一个分支中间件,用于在中间件中进行分支处理。
**签名**
* `branch(map: { [key: string]: Function }, reducer: Function, options): Function`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `map` | `{ [key: string]: Function }` | - | 分支处理函数映射表,键名由后续计算函数在调用时给出 |
| `reducer` | `(ctx) => string` | - | 计算函数,用于基于上下文计算出分支的键名 |
| `options?` | `Object` | - | 分支配置项 |
| `options.keyNotFound?` | `Function` | `ctx.throw(404)` | 未找到键名时的处理函数 |
| `options.handlerNotSet?` | `Function` | `ctx.throw(404)` | 未定义处理函数时的处理 |
**示例**
用户验证时,根据请求 URL 中 query 部分的 `authenticator` 参数的值决定后续需要如何处理:
```ts
app.resourcer.use(branch({
'password': async (ctx, next) => {
// ...
},
'sms': async (ctx, next) => {
// ...
},
}, (ctx) => {
return ctx.action.params.authenticator ?? 'password';
}));
```

View File

@ -0,0 +1,114 @@
# Resource
Resource 用于定义资源实例。被 Resourcer 管理的资源实例都可以通过 HTTP 请求访问。
## 构造函数
用于创建 Resource 实例。通常由 Resourcer 管理器的 `define()` 接口调用替代,不需要直接使用。
**签名**
* `constructor(options: ResourceOptions, resourcer: Resourcer)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.name` | `string` | - | 资源名称,对应 URL 路由中的资源地址部分。 |
| `options.type` | `string` | `'single'` | 资源类型,可选项为 `'single'`、`'hasOne'`、`'hasMany'`、`'belongsTo'`、`'belongsToMany'`。 |
| `options.actions` | `Object` | - | 对资源可进行的操作列表,详见示例部分。 |
| `options.middlewares` | `MiddlewareType \| MiddlewareType[]` | - | 对当前定义资源进行任意操作访问时的中间件列表,详见示例部分。 |
| `options.only` | `ActionName[]` | `[]` | 针对全局操作的白名单列表,当数组中有值时(`length > 0`),只有数组中的操作可被访问。 |
| `options.except` | `ActionName[]` | `[]` | 针对全局操作的黑名单列表,当数组中有值时(`length > 0`),除数组中的操作外,其他操作可被访问。 |
| `resourcer` | `Resourcer` | - | 所属资源管理器实例。 |
**示例**
```ts
app.resourcer.define({
name: 'books',
actions: {
// 扩展的 action
publish(ctx, next) {
ctx.body = 'ok';
}
},
middleware: [
// 扩展的中间件
async (ctx, next) => {
await next();
}
]
});
```
## 实例成员
### `options`
当前资源的配置项。
### `resourcer`
所属的资源管理器实例。
### `middlewares`
已注册的中间件列表。
### `actions`
已注册的操作映射表。
### `except`
操作排除的名单列表。
## 实例方法
### `getName()`
获取当前资源的名称。
**签名**
* `getName(): string`
**示例**
```ts
const resource = app.resourcer.define({
name: 'books'
});
resource.getName(); // 'books'
```
### `getAction()`
根据名称获取当前资源的操作。
**签名**
* `getAction(name: string): Action`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 操作名称。 |
**示例**
```ts
const resource = app.resourcer.define({
name: 'books',
actions: {
publish(ctx, next) {
ctx.body = 'ok';
}
}
});
resource.getAction('publish'); // [Function: publish]
```

View File

@ -1,32 +1,32 @@
# JavaScript SDK
# @nocobase/sdk
## APIClient
```ts
class APIClient {
// axios instance
// axios 实例
axios: AxiosInstance;
// constructors
// 构造器
constructor(instance?: AxiosInstance | AxiosRequestConfig);
// Client-side requests, support for AxiosRequestConfig and ResourceActionOptions
// 客户端请求,支持 AxiosRequestConfig 和 ResourceActionOptions
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R>;
// Get Resources
// 获取资源
resource<R = IResource>(name: string, of?: any): R;
}
```
Initialize instance
初始化实例
```ts
import axios from 'axios';
import { APIClient } from '@nocobase/sdk';
// Provide AxiosRequestConfig configuration parameters
// 提供 AxiosRequestConfig 配置参数
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
// Provide AxiosInstance
// 提供 AxiosInstance
const instance = axios.create({
baseURL: 'https://localhost:8000/api',
});
@ -54,7 +54,7 @@ await api.request({ url: 'users:get' });
## Storage
APIClient uses localStorage by default, you can also custom storage.
APIClient 默认使用的 localStorage你也可以自定义 Storage
```ts
import { Storage } from '@nocobase/sdk';
@ -88,19 +88,19 @@ const api = new APIClient({
## Auth
```ts
// sign in and remember the current token
// 登录并记录 token
api.auth.signIn({ email, password });
// sign out and delete the token
// 注销并删除 token
api.auth.signOut();
// set the token
// 设置 token
api.auth.setToken('123');
// set the role (multiple roles)
// 设置 role当需要多角色时
api.auth.setRole('admin');
// set the locale (multiple languages)
// 设置 locale当需要多语言时
api.auth.setLocale('zh-CN');
```
Custom Auth
自定义 Auth
```ts
import { Auth } from '@nocobase/sdk';

View File

@ -0,0 +1,188 @@
# Application
基于 [Koa](https://koajs.com/) 实现的 WEB 框架,兼容所有的 Koa 插件。
## 构造函数
### `constructor()`
创建一个应用实例。
**签名**
* `constructor(options: ApplicationOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.database` | `IDatabaseOptions` or `Database` | `{}` | 数据库配置 |
| `options.resourcer` | `ResourcerOptions` | `{}` | 资源路由配置 |
| `options.cors` | [`CorsOptions`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/koa__cors/index.d.ts#L24) | `{}` | 跨域配置,参考 [@koa/cors](https://npmjs.com/package/@koa/cors) |
| `options.dataWrapping` | `boolean` | `true` | 是否包装响应数据,`true` 则将把通常的 `ctx.body` 包装为 `{ data, meta }` 的结构。 |
| `options.registerActions` | `boolean` | `true` | 是否注册默认的 [actions](#) |
| `options.i18n` | `I18nOptions` | `{}` | 国际化配置,参考 [i18next](https://www.i18next.com/overview/api) |
| `options.plugins` | `PluginConfiguration[]` | `[]` | 默认启用的插件配置 |
Type
```ts
interface ApplicationOptions {
}
```
**示例**
```ts
import Application from '@nocobase/server';
const app = new Application({
database: {
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'test',
},
resourcer: {
prefix: '/api',
},
cors: {
origin: '*',
}
});
```
## 实例成员
### `cli`
命令行工具实例,参考 npm 包 [Commander](https://www.npmjs.com/package/commander)。
### `db`
数据库实例,相关 API 参考 [Database](/api/database)。
### `resourcer`
应用初始化自动创建的资源路由管理实例,相关 API 参考 [Resourcer](/api/resourcer)。
### `acl`
ACL 实例,相关 API 参考 [ACL](/api/acl)。
### `i18n`
I18next 实例,相关 API 参考 [I18next](https://www.i18next.com/overview/api)。
### `pm`
插件管理器实例,相关 API 参考 [PluginManager](./plugin-manager)。
### `version`
应用版本实例,相关 API 参考 [ApplicationVersion](./application-version)。
### `middleware`
内置的中间件有:
- i18next
- bodyParser
- cors
- dataWrapping
- collection2resource
- restApiMiddleware
### `context`
继承自 koa 的 context可以通过 `app.context` 访问,用于向每个请求注入上下文可访问的内容。参考 [Koa Context](https://koajs.com/#app-context)。
NocoBase 默认对 context 注入了以下成员,可以在请求处理函数中直接使用:
| 变量名 | 类型 | 描述 |
| --- | --- | --- |
| `ctx.app` | `Application` | 应用实例 |
| `ctx.db` | `Database` | 数据库实例 |
| `ctx.resourcer` | `Resourcer` | 资源路由管理器实例 |
| `ctx.action` | `Action` | 资源操作相关对象实例 |
| `ctx.i18n` | `I18n` | 国际化实例 |
| `ctx.t` | `i18n.t` | 国际化翻译函数快捷方式 |
| `ctx.getBearerToken()` | `Function` | 获取请求头中的 bearer token |
## 实例方法
### `use()`
注册中间件,兼容所有 [Koa 插件](https://www.npmjs.com/search?q=koa)
### `on()`
订阅应用级事件,主要与生命周期相关,等同于 `eventEmitter.on()`。所有可订阅事件参考 [事件](#事件)。
### `command()`
自定义 command
### `findCommand()`
查找已定义 command
### `runAsCLI()`
以 CLI 的方式运行。
### `load()`
加载应用配置。
**签名**
* `async load(): Promise<void>`
### `reload()`
重载应用配置。
### `install()`
初始化安装应用,同步安装插件。
### `upgrade()`
升级应用,同步升级插件。
### `start()`
启动应用,如果配置了监听的端口,将启动监听,之后应用即可接受 HTTP 请求。
**签名**
* `async start(options: StartOptions): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.listen?` | `ListenOptions` | `{}` | HTTP 监听参数对象 |
| `options.listen.port?` | `number` | 13000 | 端口 |
| `options.listen.host?` | `string` | `'localhost'` | 域名 |
### `stop()`
停止应用,此方法会关闭数据库连接,关闭 HTTP 端口,不会删除数据。
### `destroy()`
删除应用,此方法会删除应用对应的数据库。
## 事件
### `'beforeLoad'` / `'afterLoad'`
### `'beforeInstall'` / `'afterInstall'`
### `'beforeUpgrade'` / `'afterUpgrade'`
### `'beforeStart'` / `'afterStart'`
### `'beforeStop'` / `'afterStop'`
### `'beforeDestroy'` / `'afterDestroy'`

View File

@ -0,0 +1,59 @@
# PluginManager
应用插件管理器的实例,由应用自动创建,可以通过 `app.pm` 访问。
## 实例方法
### `create()`
在本地创建一个插件脚手架
**签名**
```ts
create(name, options): void;
```
### `addStatic()`
**签名**
```ts
addStatic(plugin: any, options?: PluginOptions): Plugin;
```
**示例**
```ts
pm.addStatic('nocobase');
```
### `add()`
**签名**
```ts
async add(plugin: any, options?: PluginOptions): Promise<Plugin>;
async add(plugin: string[], options?: PluginOptions): Promise<Plugin[]>;
```
**示例**
```ts
await pm.add(['test'], {
builtIn: true,
enabled: true,
});
```
### `get()`
获取插件实例
### `enable()`
### `disable()`
### `remove()`
### `upgrade()`

View File

@ -0,0 +1,59 @@
# Plugin
## 示例
```ts
const app = new Application();
class MyPlugin extends Plugin {
afterAdd() {}
beforeLoad() {}
load() {}
install() {}
afterEnable() {}
afterDisable() {}
remove() {}
}
app.plugin(MyPlugin, { name: 'my-plugin' });
```
## 属性
### `options`
插件配置信息
### `name`
插件标识,只读
## 实例方法
### `afterAdd()`
插件 add/addStatic 之后
### `beforeLoad()`
插件加载前,如事件或类注册
### `load()`
加载插件,配置之类
### `install()`
插件安装逻辑,如初始化数据
### `afterEnable()`
插件激活之后的逻辑
### `afterDisable()`
插件禁用之后的逻辑
### `remove()`
用于实现插件删除逻辑

View File

@ -1,24 +1,18 @@
# Directory structure
# 项目目录结构
## Application scaffolding
```bash
$ yarn create nocobase-app my-nocobase-app
```
The directory structure of the application scaffold created by `create-nocobase-app` is as follows
无论是源码还是 `create-nocobase-app` 创建的应用,目录结构都是一样的,结构如下:
```bash
├── my-nocobase-app
├── packages # Use the Monorepo approach to manage code, dividing different modules into packages
├── packages # 采用 Monorepo 的方式管理代码,将不同模块划分到不同包里
├── app
├── client # Client-side modules
├── server # Server-side modules
├── plugins # Plugins directory
├── storage # For database files, attachments, cache, etc.
├── client # 客户端模块
├── server # 服务端模块
├── plugins # 插件目录
├── storage # 用于存放数据库文件、附件、缓存等
├── db
├── .env # Environment variables
├── .buildrc.ts # Packaging configuration for packages, supports cjs, esm and umd packaging.
├── .env # 环境变量
├── .buildrc.ts # packages 的打包配置,支持 cjs、esm 和 umd 三种格式的打包。
├── jest.config.js
├── jest.setup.ts
├── lerna.json
@ -28,7 +22,7 @@ The directory structure of the application scaffold created by `create-nocobase-
├── tsconfig.server.json
```
### packages directory
### packages 目录
```bash
├── packages
@ -51,39 +45,20 @@ The directory structure of the application scaffold created by `create-nocobase-
├── package.json
```
NocoBase uses the Monorepo approach to manage the code, dividing the different modules into different packages.
NocoBase 采用 Monorepo 的方式管理代码,将不同模块划分到不同包里。
- `app/client` is the client-side module of the application, built on [umi](https://umijs.org).
- `app/server` is the server-side module of the application.
- `plugins/*` directory can hold various plugins.
- `app/client` 为应用的客户端模块,基于 [umi](https://umijs.org/zh-CN) 构建;
- `app/server` 为应用的服务端模块;
- `plugins/*` 目录里可以放各种插件。
### storages directory
### storages 目录
Used to store database files, attachments, cache, etc.
用于存放数据库文件、附件、缓存等。
### .env file
### .env 文件
Environment variables
环境变量。
### .buildrc.ts file
### .buildrc.ts 文件
Packaging configuration for packages, supports cjs, esm and umd packaging.
## Plugins scaffolding
```bash
$ yarn nocobase create-plugin my-plugin
```
The plugin scaffolding directory initialized by `nocobase create-plugin` is as follows
```bash
├── my-nocobase-app
├── packages
├── plugins
├── my-plugin
├── src
├── client
├── server
├── package.json
```
packages 的打包配置,支持 cjs、esm 和 umd 三种格式的打包。

View File

@ -1,207 +0,0 @@
# Environment variables
## Global environment variables
Saved in the `.env` file
### APP_ENV
Application environment, default value `development`, options include
- `production` production environment
- `development` development environment
```bash
APP_ENV=production
```
### APP_HOST
Application host, default value `0.0.0.0`
```bash
APP_HOST=192.168.3.154
```
### APP_PORT
Application port, default value `13000`
```bash
APP_PORT=13000
```
### APP_KEY
Secret key for scenarios such as jwt
```bash
APP_KEY=app-key-test
```
### API_BASE_PATH
NocoBase API address prefix, default value `/api/`
```bash
API_BASE_PATH=/api/
```
### DB_DIALECT
Database type, default value `sqlite`options include
- `sqlite`
- `mysql`
- `postgres`
```bash
DB_DIALECT=mysql
```
### DB_STORAGE
Database file path (configured when using SQLite)
```bash
# Relative path
DB_HOST=storage/db/nocobase.db
# Absolute path
DB_HOST=/your/path/nocobase.db
```
### DB_HOST
Database host (required when using MySQL or PostgreSQL)
Default value `localhost`
```bash
DB_HOST=localhost
```
### DB_PORT
Database port (required when using MySQL or PostgreSQL)
- MySQL default port 3306
- PostgreSQL default port 5432
```bash
DB_PORT=3306
```
### DB_DATABASE
Database name (required when using MySQL or PostgreSQL)
```bash
DB_DATABASE=nocobase
```
### DB_USER
Database user (required when using MySQL or PostgreSQL)
```bash
DB_USER=nocobase
```
### DB_PASSWORD
Database password (required when using MySQL or PostgreSQL)
```bash
DB_PASSWORD=nocobase
```
### DB_TABLE_PREFIX
Data Table Prefix
```bash
DB_TABLE_PREFIX=nocobase_
```
### DB_LOGGING
Switching of logs, default value `off`, options include
- `on` On
- `off` Off
```bash
DB_LOGGING=on
```
## Temporary environment variables
When installing NocoBase, you can assist in the installation by setting temporary environment variables, such as
```bash
yarn cross-env \
INIT_APP_LANG=en-US \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
# Equivalent to
yarn nocobase install \
--lang=en-US \
--root-email=demo@nocobase.com \
--root-password=admin123 \
--root-nickname="Super Admin"
# Equivalent to
yarn nocobase install -l en-US -e demo@nocobase.com -p admin123 -n "Super Admin"
```
### INIT_APP_LANG
Language at installation, default value `en-US`, options include
- `en-US`
- `zh-CN`
```bash
yarn cross-env \
INIT_APP_LANG=en-US \
nocobase install
```
### INIT_ROOT_EMAIL
Root user's email
```bash
yarn cross-env \
INIT_APP_LANG=en-US \
INIT_ROOT_EMAIL=demo@nocobase.com \
nocobase install
```
### INIT_ROOT_PASSWORD
Root user's password
```bash
yarn cross-env \
INIT_APP_LANG=en-US \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
nocobase install
```
### INIT_ROOT_NICKNAME
Root user's name
```bash
yarn cross-env \
INIT_APP_LANG=en-US \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
```

View File

@ -0,0 +1,319 @@
# 数据表与字段
## 基础概念
数据建模是一个应用最底层的基础,在 NocoBase 应用中我们通过数据表Collection和字段Field来进行数据建模并且建模也将映射到数据库表以持久化。
### Collection
Collection 是所有同类数据的集合,在 NocoBase 中对应数据库表的概念,如订单、商品、用户、评论等都可以形成 Collection 定义,不同 Collection 通过 name 区分,包含的字段由 `fields` 定义,如:
```ts
db.collection({
name: 'posts',
fields: [
{ name: 'title', type: 'string' },
{ name: 'content', type: 'text' },
// ...
]
});
```
定义完成后 collection 暂时只处于内存中,还需要调用 [`db.sync()`](/api/database#sync) 方法将其同步到数据库中。
### Field
对应数据库表“字段”的概念每个数据表Collection都可以有若干 Fields例如
```ts
db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'integer', name: 'age' },
// 其他字段
],
});
```
其中字段名称(`name`)和字段类型(`type`)是必填项,不同字段通过字段名(`name`)区分,除 `name``type` 以外,根据不同字段类型可以有更多的配置信息。所有数据库字段类型及配置详见 API 参考的[内置字段类型列表](/api/database/field#内置字段类型列表)部分。
## 示例
对于开发者,通常我们会建立与普通数据表不同的一些功能型数据表,并把这些数据表固化成插件的一部分,并结合其他数据处理流程以形成完整的功能。
接下来我们以一个简单的在线商店插件为例来介绍如何建模并管理插件的数据表。假设你已经学习过 [编写第一个插件](/development/your-first-plugin),我们继续在之前的插件代码上开发,只不过插件的名称从 `hello` 改为 `shop-modeling`
### 插件中定义并创建数据表
对于一个店铺,首先需要建立一张商品的数据表,命名为 `products`。与直接调用 [`db.collection()`](/api/database#collection) 这样的方法稍有差异,在插件中我们会使用更方便的方法一次性导入多个文件定义的数据表。所以我们先为商品数据表的定义创建一个文件命名为 `collections/products.ts`,填入以下内容:
```ts
export default {
name: 'products',
fields: [
{
type: 'string',
name: 'title'
},
{
type: 'integer',
name: 'price'
},
{
type: 'boolean',
name: 'enabled'
},
{
type: 'integer',
name: 'inventory'
}
]
};
```
可以看到NocoBase 的数据库表结构定义可以直接使用标准的 JSON 格式,其中 `name``fields` 都是必填项,代表数据表名和该表中的字段定义。字段定义中与 Sequelize 类似会默认创建主键(`id`)、数据创建时间(`createdAt`)和数据更新时间(`updatedAt`)等系统字段,如有特殊需要可以以同名的配置覆盖定义。
该文件定义的数据表我们可以在插件主类的 `load()` 周期中使用 `db.import()` 引入并完成定义。如下所示:
```ts
import path from 'path';
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.acl.allow('products', '*');
this.app.acl.allow('categories', '*');
this.app.acl.allow('orders', '*');
}
}
```
同时我们为了方便测试,先暂时允许针对这几张表数据资源的所有访问权限,后面我们会在 [权限管理](/development/guide/acl) 中详细介绍如何管理资源的权限。
这样在插件被主应用加载时,我们定义的 `products` 表也就被加载到数据库管理实例的内存中了。同时,基于 NocoBase 约定式的数据表资源映射,在应用的服务启动以后,会自动生成对应的 CRUD HTTP API。
当从客户端请求以下 URL 时,会得到对应的响应结果:
* `GET /api/products:list`:获取所有商品数据列表
* `GET /api/products:get?filterByTk=<id>`:获取指定 ID 的商品数据
* `POST /api/products`:创建一条新的商品数据
* `PUT /api/products:update?filterByTk=<id>`:更新一条商品数据
* `DELETE /api/products:destroy?filterByTk=<id>`:删除一条商品数据
### 定义关系表和关联字段
在上面的例子中,我们只定义了一个商品数据表,但是实际上一个商品还需要关联到一个分类,一个品牌,一个供应商等等。这些关联关系可以通过定义关系表来实现,比如我们可以定义一个 `categories` 表,用来关联商品和分类,然后在商品表中添加一个 `category` 字段来关联到分类表。
新增文件 `collections/categories.ts`,并填入内容:
```ts
export default {
name: 'categories',
fields: [
{
type: 'string',
name: 'title'
},
{
type: 'hasMany',
name: 'products',
}
]
};
```
我们为 `categories` 表定义了两个字段,一个是标题,另一个是该分类下关联的所有产品的一对多字段,会在后面一起介绍。因为我们已经在插件的主类中使用了 `db.import()` 方法导入 `collections` 目录下的所有数据表定义,所以这里新增的 `categories` 表也会被自动导入到数据库管理实例中。
修改文件 `collections/products.ts`,在 `fields` 中添加一个 `category` 字段:
```ts
{
name: 'products',
fields: [
// ...
{
type: 'belongsTo',
name: 'category',
target: 'categories',
}
]
}
```
可以看到,我们为 `products` 表新增的 `category` 字段是一个 `belongsTo` 类型的字段,它的 `target` 属性指向了 `categories` 表,这样就定义了一个 `products` 表和 `categories` 表之间的多对一关系。同时结合我们在 `categories` 表中定义的 `hasMany` 字段,就可以实现一个商品可以关联到多个分类,一个分类下可以有多个商品的关系。通常 `belongsTo``hasMany` 可以成对出现,分别定义在两张表中。
定义好两张表之间的关系后,同样的我们就可以直接通过 HTTP API 来请求关联数据了:
* `GET /api/products:list?appends=category`:获取所有商品数据,同时包含关联的分类数据
* `GET /api/products:get?filterByTk=<id>&appends=category`:获取指定 ID 的商品数据,同时包含关联的分类数据
* `GET /api/categories/<categoryId>/products:list`:获取指定分类下的所有商品数据
* `POST /api/categories/<categoryId>/products`:在指定分类下创建新的商品
与一般的 ORM 框架类似NocoBase 内置了四种关系字段类型,更多信息可以参考 API 字段类型相关的章节:
* [`belongsTo` 类型](/api/database/field#belongsto)
* [`belongsToMany` 类型](/api/database/field#belongstomany)
* [`hasMany` 类型](/api/database/field#hasmany)
* [`hasOne` 类型](/api/database/field#hasone)
### 扩展已有数据表
在上面的例子中,我们已经有了商品表和分类表,为了提供销售流程,我们还需要一个订单表。我们可以在 `collections` 目录下新增一个 `orders.ts` 文件,然后定义一个 `orders` 表:
```ts
export default {
name: 'orders',
fields: [
{
type: 'uuid',
name: 'id',
primaryKey: true
},
{
type: 'belongsTo',
name: 'product'
},
{
type: 'integer',
name: 'quantity'
},
{
type: 'integer',
name: 'totalPrice'
},
{
type: 'integer',
name: 'status'
},
{
type: 'string',
name: 'address'
},
{
type: 'belongsTo',
name: 'user'
}
]
}
```
为了简化,订单表中与商品的关联我们只简单的定义为多对一关系,而在实际业务中可能会用到多对多或快照等复杂的建模方式。可以看到,一个订单除了对应某个商品,我们还增加了一个对应用户的关系定义,用户是 NocoBase 内置插件管理的数据表(详细参考[用户插件的代码](https://github.com/nocobase/nocobase/tree/main/packages/plugins/users)),如果我们希望针对已存在的用户表扩展定义“一个用户所拥有的多个订单”的关系,可以在当前的 shop-modeling 插件内继续新增一个数据表文件 `collections/users.ts`,与直接导出 JSON 数据表配置不同的是,这里使用 `@nocobase/database` 包的 `extend()` 方法,进行对已有数据表的扩展定义:
```ts
import { extend } from '@nocobase/database';
export extend({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'orders'
}
]
});
```
这样,原先已存在的用户表也就拥有了一个 `orders` 关联字段,我们可以通过 `GET /api/users/<userId>/orders:list` 来获取指定用户的所有订单数据。
这个方法在扩展其他已有插件已定义的数据表时非常有用,使得其他已有插件不会反向依赖新的插件,仅形成单向依赖关系,方便在扩展层面进行一定程度的解耦。
### 扩展字段类型
我们在定义订单表的时候针对 `id` 字段使用了 `uuid` 类型,这是一个内置的字段类型,有时候我们也会觉得 UUID 看起来太长比较浪费空间,且查询性能不佳,希望用一个更适合的字段类型,比如一个含日期信息等复杂的编号逻辑,或者是 Snowflake 算法,我们就需要扩展一个自定义字段类型。
假设我们需要直接应用 Snowflake ID 生成算法,扩展出一个 `snowflake` 字段类型,我们可以创建一个 `fields/snowflake.ts` 文件:
```ts
import { DataTypes } from 'sequelize';
// 引入算法工具包
import { Snowflake } from 'nodejs-snowflake';
// 引入字段类型基类
import { Field, BaseColumnFieldOptions } from '@nocobase/database';
export interface SnowflakeFieldOptions extends BaseColumnFieldOptions {
type: 'snowflake';
epoch: number;
instanceId: number;
}
export class SnowflakeField extends Field {
get dataType() {
return DataTypes.BIGINT;
}
constructor(options: SnowflakeFieldOptions, context) {
super(options, context);
const {
epoch: custom_epoch,
instanceId: instance_id = process.env.INSTANCE_ID ? Number.parseInt(process.env.INSTANCE_ID) : 0,
} = options;
this.generator = new Snowflake({ custom_epoch, instance_id });
}
setValue = (instance) => {
const { name } = this.options;
instance.set(name, this.generator.getUniqueID());
};
bind() {
super.bind();
this.on('beforeCreate', this.setValue);
}
unbind() {
super.unbind();
this.off('beforeCreate', this.setValue);
}
}
export default SnowflakeField;
```
之后在插件主文件向数据库注册新的字段类型:
```ts
import SnowflakeField from './fields/snowflake';
export default class ShopPlugin extends Plugin {
initialize() {
// ...
this.db.registerFieldTypes({
snowflake: SnowflakeField
});
// ...
}
}
```
这样,我们就可以在订单表中使用 `snowflake` 字段类型了:
```ts
export default {
name: 'orders',
fields: [
{
type: 'snowflake'
name: 'id',
primaryKey: true
},
// ...other fields
]
}
```
## 小结
通过上面的示例,我们基本了解了如何在一个插件中进行数据建模,包括:
* 定义数据表和普通字段
* 定义关联表和关联字段关系
* 扩展已有的数据表的字段
* 扩展新的字段类型
我们将本章所涉及的代码放到了一个完整的示例包 [packages/samples/shop-modeling](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-modeling) 中,可以直接在本地运行,查看效果。

View File

@ -0,0 +1,98 @@
# 命令行
## 简介
NocoBase Server Application 除了用作 WEB 服务器以外,也是个强大可扩展的 CLI 工具。
新建一个 `app.js` 文件,代码如下:
```ts
const Application = require('@nocobase/server');
// 此处省略具体配置
const app = new Application({/*...*/});
app.runAsCLI();
```
`runAsCLI()` 方式运行的 app.js 是一个 CLI在命令行工具就可以像这样操作了
```bash
node app.js install # 安装
node app.js start # 启动
```
为了更好的开发、构建和部署 NocoBase 应用NocoBase 内置了许多命令,详情查看 [NocoBase CLI](/api/cli) 章节。
## 自定义 Command
NocoBase CLI 的设计思想与 [Laravel Artisan](https://laravel.com/docs/9.x/artisan) 非常相似都是可扩展的。NocoBase CLI 基于 [commander](https://www.npmjs.com/package/commander) 实现,可以这样扩展 Command
```ts
app
.command('echo')
.option('-v, --version');
.action(async ([options]) => {
console.log('Hello World!');
if (options.version) {
console.log('Current version:', app.getVersion());
}
});
```
这个方法定义了以下命令:
```bash
yarn nocobase echo
# Hello World!
yarn nocobase echo -v
# Hello World!
# Current version: 0.7.4-alpha.7
```
更多 API 细节可参考 [Application.command()](/api/server/application#command) 部分。
## 示例
### 定义导出数据表的命令
如果我们希望把应用的数据表中的数据导出成 JSON 文件,可以定义一个如下的子命令:
```ts
import path from 'path';
import * as fs from 'fs/promises';
class MyPlugin extends Plugin {
load() {
this.app
.command('export')
.option('-o, --output-dir')
.action(async (options, ...collections) => {
const { outputDir = path.join(process.env.PWD, 'storage') } = options;
await collections.reduce((promise, collection) => promise.then(async () => {
if (!this.db.hasCollection(collection)) {
console.warn('No such collection:', collection);
return;
}
const repo = this.db.getRepository(collection);
const data = repo.find();
await fs.writeFile(path.join(outputDir, `${collection}.json`), JSON.stringify(data), { mode: 0o644 });
}), Promise.resolve());
});
}
}
```
注册和激活插件之后在命令行调用:
```bash
mkdir -p ./storage/backups
yarn nocobase export -o ./storage/backups users
```
执行后会生成 `./storage/backups/users.json` 文件包含数据表中的数据。
## 小结
本章所涉及示例代码整合在 [packages/samples/command](https://github.com/nocobase/nocobase/tree/main/packages/samples/command) 包中,可以直接在本地运行,查看效果。

View File

@ -0,0 +1,176 @@
# 事件
事件在很多插件化可扩展的框架和系统中都有应用,比如著名的 Wordpress是比较广泛的对生命周期支持扩展的机制。
## 基础概念
NocoBase 在应用生命周期中提供了一些钩子,以便在运行中的一些特殊时期根据需要进行扩展开发。
### 数据库事件
主要通过 `db.on()` 的方法定义,大部分事件兼容 Sequelize 原生的事件类型。例如需要在某张数据表创建一条数据后做一些事情时,可以使用 `<collectionName>.afterCreate` 事件:
```ts
// posts 表创建数据完成时触发
db.on('posts.afterCreate', async (post, options) => {
console.log(post);
});
```
由于 Sequelize 默认的单条数据创建成功触发的时间点上并未完成与该条数据所有关联数据的处理,所以 NocoBase 针对默认封装的 Repository 数据仓库类完成数据创建和更新操作时,扩展了几个额外的事件,代表关联数据被一并操作完成:
```ts
// 已创建且已根据创建数据完成关联数据创建或更新完成时触发
db.on('posts.afterCreateWithAssociations', async (post, options) => {
console.log(post);
});
```
与 Sequelize 同样的也可以针对全局的数据处理都定义特定的事件:
```ts
// 每张表创建数据完成都触发
db.on('beforeCreate', async (model, options) => {
console.log(model);
});
```
针对特殊的生命周期比如定义数据表等NocoBase 也扩展了相应事件:
```ts
// 定义任意数据表之前触发
db.on('beforeDefineCollection', (collection) => {
collection.options.tableName = 'somePrefix' + collection.options.tableName;
});
```
其他所有可用的数据库事件类型可以参考 [Database API](/api/database#on)。
### 应用级事件
在某些特殊需求时,会需要在应用的外层生命周期中定义事件进行扩展,比如当应用启动前做一些准备操作,当应用停止前做一些清理操作等:
```ts
app.on('beforeStart', async () => {
console.log('app is starting...');
});
app.on('beforeStop', async () => {
console.log('app is stopping...');
});
```
其他所有可用的应用级事件类型可以参考 [Application API](/api/server/application#事件)。
## 示例
我们继续以简单的在线商店来举例,相关的数据表建模可以回顾 [数据表和字段](/development/) 部分的示例。
### 创建订单后减商品库存
通常我们的商品和订单是不同的数据表,而客户在下单以后把商品的库存减掉可以解决超卖的问题,这时候我们可以针对创建订单这个数据操作定义相应的事件,在这个时机一并解决库存修改的问题:
```ts
class ShopPlugin extends Plugin {
load() {
this.db.on('orders.afterCreate', async (order, options) => {
const product = await order.getProduct({
transaction: options.transaction
});
await product.update({
inventory: product.inventory - order.quantity
}, {
transaction: options.transaction
});
});
}
}
```
因为默认 Sequelize 的事件中就携带事务等信息,所以我们可以直接使用 transaction 以保证两个数据操作都在同一事务中进行。
同样的,也可以在创建发货记录后修改订单状态为已发货:
```ts
class ShopPlugin extends Plugin {
load() {
this.db.on('deliveries.afterCreate', async (delivery, options) => {
const orderRepo = this.db.getRepository('orders');
await orderRepo.update({
filterByTk: delivery.orderId,
value: {
status: 2
}
transaction: options.transaction
});
});
}
}
```
### 随应用同时存在的定时任务
在不考虑使用工作流插件等复杂情况下,我们也可以通过应用级的事件实现一个简单的定时任务机制,且可以与应用的进程绑定,退出后就停止。比如我们希望定时扫描所有订单,超过签收时间后自动签收:
```ts
class ShopPlugin extends Plugin {
timer = null;
orderReceiveExpires = 86400 * 7;
checkOrder = async () => {
const expiredDate = new Date(Date.now() - this.orderReceiveExpires);
const deliveryRepo = this.db.getRepository('deliveries');
const expiredDeliveries = await deliveryRepo.find({
fields: ['id', 'orderId'],
filter: {
status: 0,
createdAt: {
$lt: expiredDate
}
}
});
await deliveryRepo.update({
filter: {
id: expiredDeliveries.map(item => item.get('id')),
},
values: {
status: 1
}
});
const orderRepo = this.db.getRepository('orders');
const [updated] = await orderRepo.update({
filter: {
status: 2,
id: expiredDeliveries.map(item => item.get('orderId'))
},
values: {
status: 3
}
});
console.log('%d orders expired', updated);
};
load() {
this.app.on('beforeStart', () => {
// 每分钟执行一次
this.timer = setInterval(this.checkOrder, 1000 * 60);
});
this.app.on('beforeStop', () => {
clearInterval(this.timer);
this.timer = null;
});
}
}
```
## 小结
通过上面的示例,我们基本了解了事件的作用和可以用于扩展的方式:
* 数据库相关的事件
* 应用相关的事件
本章涉及的示例代码整合在对应的包 [packages/samples/shop-events](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-events) 中,可以直接在本地运行,查看效果。

View File

@ -0,0 +1,266 @@
# 国际化
## 基础概念
多语言国际化支持根据服务端和客户端分为两大部分,各自有相应的实现。
语言配置均为普通的 JSON 对象键值对,如果没有翻译文件(或省略编写)的话会直接输出键名的字符串。
### 服务端
服务端的多语言国际化基于 npm 包 [i18next](https://npmjs.com/package/i18next) 实现,在服务端应用初始化时会创建一个 i18next 实例,同时也会将此实例注入到请求上下文(`context`)中,以供在各处方便的使用。
在创建服务端 Application 实例时可以传入配置对应的初始化参数:
```ts
import { Application } from '@nocobase/server';
const app = new Application({
i18n: {
defaultNS: 'test',
resources: {
'en-US': {
test: {
hello: 'Hello',
},
},
'zh-CN': {
test: {
hello: '你好',
}
}
}
}
});
```
或者在插件中对已存在的 app 实例添加语言数据至对应命名空间:
```ts
app.i18n.addResources('zh-CN', 'test', {
Hello: '你好',
World: '世界',
});
app.i18n.addResources('en-US', 'test', {
Hello: 'Hello',
World: 'World',
});
```
基于应用:
```ts
app.i18n.t('World') // “世界”或“World”
```
基于请求:
```ts
app.resource({
name: 'test',
actions: {
async get(ctx, next) {
ctx.body = `${ctx.t('Hello')} ${ctx.t('World')}`;
await next();
}
}
});
```
通常服务端的多语言处理主要用于错误信息的输出。
### 客户端
客户端的多语言国际化基于 npm 包 [react-i18next](https://npmjs.com/package/react-i18next) 实现,在应用顶层提供了 `<I18nextProvider>` 组件的包装,可以在任意位置直接使用相关的方法。
添加语言包:
```tsx | pure
import { i18n } from '@nocobase/client';
i18n.addResources('zh-CN', 'test', {
Hello: '你好',
World: '世界',
});
```
注:这里第二个参数填写的 `'test'` 是语言的命名空间通常插件自己定义的语言资源都应该按自己插件包名创建特定的命名空间以避免和其他语音资源冲突。NocoBase 中默认的命名空间是 `'client'`,大部分常用和基础的语言翻译都放置在此命名空间,当没有提供所需语言时,可在插件自身的命名空间内进行扩展定义。
在组件中调用翻译函数:
```tsx | pure
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function MyComponent() {
// 使用之前定义的命名空间
const { t } = useTranslation('test');
return (
<div>
<p>{t('World')}</p>
</div>
);
}
```
在 SchemaComponent 组件中可以直接使用模板方法 `'{{t(<languageKey>)}}'`,模板中的翻译函数会自动被执行:
```tsx | pure
import React from 'react';
import { SchemaComponent } from '@nocobase/client';
export default function MySchemaComponent() {
return (
<SchemaComponent
schema={{
type: 'string',
'x-component': 'Input',
'x-component-props': {
value: '{{t("Hello", { ns: "test" })}}'
},
}}
/>
);
}
```
在某些特殊情况下也需要以模板的方式定义多语言时,可以使用 NocoBase 内置的 `compile()` 方法编译为多语言结果:
```tsx | pure
import React from 'react';
import { useCompile } from '@nocobase/client';
const title = '{{t("Hello", { ns: "test" })}}';
export default function MyComponent() {
const { compile } = useCompile();
return (
<div>{compile(title)}</div>
);
}
```
### 建议配置
当添加语言资源时,推荐在 JSON 配置模板中将语言串的键名设置为默认语言,更方便统一处理,且可以省去默认语言的翻译。例如以英语为默认语言:
```ts
i18n.addResources('zh-CN', 'your-namespace', {
'Show dialog': '显示对话框',
'Hide dialog': '隐藏对话框'
});
```
语言内容如果比较多,推荐在插件中创建一个 `locals` 目录,并把对应语言文件都放置在其中方便管理:
```
- server
| - locals
| | - zh-CN.ts
| | - en-US.ts
| | - ...
| - index.ts
```
## 示例
### 服务端错误提示
例如用户在店铺对某个商品下单时,如果商品的库存不够,或者未上架,那么下单接口被调用时,应该返回相应的错误。
```ts
const namespace = 'shop';
export default class ShopPlugin extends Plugin {
async load() {
this.app.i18n.addResources('zh-CN', namespace, {
'No such product': '商品不存在',
'Product not on sale': '商品已下架',
'Out of stock': '库存不足',
});
this.app.resource({
name: 'orders',
actions: {
async create(ctx, next) {
const productRepo = ctx.db.getRepository('products');
const product = await productRepo.findOne({
filterByTk: ctx.action.params.values.productId
});
if (!product) {
return ctx.throw(404, ctx.t('No such product'));
}
if (!product.enabled) {
return ctx.throw(400, ctx.t('Product not on sale'));
}
if (!product.inventory) {
return ctx.throw(400, ctx.t('Out of stock'));
}
const orderRepo = ctx.db.getRepository('orders');
ctx.body = await orderRepo.create({
values: {
productId: product.id,
quantity: 1,
totalPrice: product.price,
userId: ctx.state.currentUser.id
}
});
next();
}
}
});
}
}
```
### 客户端组件多语言
例如订单状态的组件,根据不同值有不同的文本显示:
```tsx
import React from 'react';
import { Select } from 'antd';
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
i18n.addResources('zh-CN', '@nocobase/plugin-sample-shop-i18n', {
Pending: '已下单',
Paid: '已支付',
Delivered: '已发货',
Received: '已签收'
});
const ORDER_STATUS_LIST = [
{ value: -1, label: 'Canceled (untranslated)' },
{ value: 0, label: 'Pending' },
{ value: 1, label: 'Paid' },
{ value: 2, label: 'Delivered' },
{ value: 3, label: 'Received' },
]
function OrderStatusSelect() {
const { t } = useTranslation('@nocobase/plugin-sample-shop-i18n');
return (
<Select style={{ minWidth: '8em' }}>
{ORDER_STATUS_LIST.map(item => (
<Select.Option value={item.value}>{t(item.label)}</Select.Option>
))}
</Select>
);
}
export default function () {
return (
<OrderStatusSelect />
);
}
```

View File

@ -0,0 +1,71 @@
# 概述
## Web Server
- Collection & Field
- Resource & Action
- Middleware
- Events & Hooks
**Samples**
- samples/shop-modeling
- samples/shop-actions
- samples/ratelimit
- samples/model-hooks
## UI Schema Designer
平台最核心的功能,用于可视化配置页面。
- 扩展各种供 UI 设计器使用的 Schema 组件
- 各种 x-designer 和 x-initializer
**Samples**
- samples/custom-block
- samples/custom-action
- samples/custom-field
- samples/custom-x-designer
- samples/custom-x-initializer
## UI Router
用于自定义页面,包括:
- routes config
- route components
**Samples**
- samples/custom-page
## Settings Center
由插件提供的系统级配置的能力
- plugin settings tabs
**Samples**
- samples/custom-settings-center-page
## I18n
国际化
- I18n
**Samples**
- samples/shop-i18n
## Devtools
- Commands
- Migrations
**Samples**
- samples/custom-command
- samples/custom-migration

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,148 @@
# 中间件
## 添加方法
1. `app.acl.use()` 添加资源权限级中间件,在权限判断之前执行
2. `app.resourcer.use()` 添加资源级中间件,只有请求已定义的 resource 时才执行
3. `app.use()` 添加应用级中间件,每次请求都执行
## 洋葱圈模型
```ts
app.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(1);
await next();
ctx.body.push(2);
});
app.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(3);
await next();
ctx.body.push(4);
});
```
访问 http://localhost:13000/api/hello 查看,浏览器响应的数据是:
```js
{"data": [1,3,4,2]}
```
## 内置中间件及执行顺序
1. `cors`
2. `bodyParser`
3. `i18n`
4. `dataWrapping`
5. `db2resource`
6. `restApi`
1. `parseToken`
2. `checkRole`
3. `acl`
1. `acl.use()` 添加的其他中间件
4. `resourcer.use()` 添加的其他中间件
5. action handler
7. `app.use()` 添加的其他中间件
也可以使用 `before``after` 将中间件插入到前面的某个 `tag` 标记的位置,如:
```ts
app.use(m1, { tag: 'restApi' });
app.resourcer.use(m2, { tag: 'parseToken' });
app.resourcer.use(m3, { tag: 'checkRole' });
// m4 将排在 m1 前面
app.use(m4, { before: 'restApi' });
// m5 会插入到 m2 和 m3 之间
app.resourcer.use(m5, { after: 'parseToken', before: 'checkRole' });
```
如果未特殊指定位置,新增的中间件的执行顺序是:
1. 优先执行 acl.use 添加的,
2. 然后是 resourcer.use 添加的,包括 middleware handler 和 action handler
3. 最后是 app.use 添加的。
```ts
app.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(1);
await next();
ctx.body.push(2);
});
app.resourcer.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(3);
await next();
ctx.body.push(4);
});
app.acl.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(5);
await next();
ctx.body.push(6);
});
app.resourcer.define({
name: 'test',
actions: {
async list(ctx, next) {
ctx.body = ctx.body || [];
ctx.body.push(7);
await next();
ctx.body.push(8);
},
},
});
```
访问 http://localhost:13000/api/hello 查看,浏览器响应的数据是:
```js
{"data": [1,2]}
```
访问 http://localhost:13000/api/test:list 查看,浏览器响应的数据是:
```js
{"data": [5,3,7,1,2,8,4,6]}
```
### resource 未定义,不执行 resourcer.use() 添加的中间件
```ts
app.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(1);
await next();
ctx.body.push(2);
});
app.resourcer.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push(3);
await next();
ctx.body.push(4);
});
```
访问 http://localhost:13000/api/hello 查看,浏览器响应的数据是:
```js
{"data": [1,2]}
```
以上示例hello 资源未定义,不会进入 resourcer所以就不会执行 resourcer 里的中间件
## 中间件用途
待补充
## 完整示例
待补充
- [samples/ratelimit](https://github.com/nocobase/nocobase/blob/main/packages/samples/ratelimit/) IP rate-limiting

View File

@ -0,0 +1,219 @@
# 数据库迁移
应用在业务发展或版本升级过程中,某些情况会需要修改数据库表或字段等信息,为保证安全无冲突且可回溯的解决数据库变更,通常的做法是使用数据库迁移的方式完成。
## 介绍
Nocobase 基于 npm 包 [Umzug](https://www.npmjs.com/package/umzug) 处理数据库迁移。并将相关功能集成在命令行的子命令 `nocobase migrator` 中,大部分操作通过该命令处理。
### 仅增加表或字段无需迁移脚本
通常如果只是增加数据表或增加字段,可以不使用数据库迁移脚本,而是直接修改数据表定义(`collection`)的内容即可。例如文章表定义(`collections/posts.ts`)初始状态:
```ts
export default {
name: 'posts',
fields: [
{
name: 'title',
type: 'string',
}
]
}
```
当需要增加一个分类字段时,直接修改原来的表结构定义文件内容:
```ts
export default {
name: 'posts',
fields: [
{
name: 'title',
type: 'string',
},
{
name: 'category',
type: 'belongsTo'
}
]
}
```
当新版本代码在环境中调用升级命令时,新的字段会以 Sequelize 中 sync 的逻辑自动同步到数据库中,完成表结构变更。
### 创建迁移文件
如果表结构的变更涉及到字段类型变更、索引调整等,需要人工创建迁移脚本文件:
```bash
yarn nocobase migrator create --name change-some-field.ts --folder path/to/migrations
```
该命令会在 `path/to/migrations` 目录中创建一个基于时间戳的迁移脚本文件 `YYYY.MM.DDTHH.mm.ss.change-some-field.ts`,内容如下:
```ts
import { Migration } from '@nocobase/server';
export default class MyMigration extends Migration {
async up() {
// TODO
}
async down() {
// TODO
}
}
```
脚本导出的主类相关 API 可以参考 [`Migration` 类](/api/server/migration)。
### 数据库操作内容
`up()`、`down()` 是一对互逆操作,升级时会调用 `up()`,降级时会调用 `down()`。大部分情况我们主要考虑升级操作。
在升级中我们有几种方式进行数据库变更:
```ts
import { Migration } from '@nocobase/server';
export default class MyMigration extends Migration {
async up() {
// 1. 针对自行管理的静态数据表,调用 Sequelize 提供的 queryInterface 实例上的方法
await this.queryInterface.changeColumn('posts', 'title', {
type: DataTypes.STRING,
unique: true // 添加索引
});
// 2. 针对被 collection-manager 插件管理的动态数据表,调用插件提供的 collections / fields 表的数据仓库方法
await this.db.getRepository('fields').update({
values: {
collectionName: 'posts',
name: 'title',
type: 'string',
unique: true // 添加索引
}
});
}
}
```
### 数据变更
除了表结构变更,也可以在迁移过程中导入需要的数据,或对数据进行调整:
```ts
import { Migration } from '@nocobase/server';
export default class MyMigration extends Migration {
async up() {
await this.sequelize.transaction(async transaction => {
const defaultCategory = await this.db.getRepository('categories').create({
values: {
title: '默认分类'
},
transaction
});
await this.db.getRepository('posts').update({
filter: {
categoryId: null
},
values: {
categoryId: defaultCategory.id
},
transaction
});
});
}
}
```
在一个脚本中有多项数据库操作时,建议使用事务保证所有操作都成功才算迁移完成。
### 执行升级
迁移脚本准备好以后,在项目目录下执行对应的升级命令即可完成数据库变更:
```bash
yarn nocobase upgrade
```
根据数据库迁移的机制,迁移脚本执行成功后也会被记录在数据库的升级记录表中,只有第一次执行有效,之后的多次重复执行都会被忽略。
## 示例
### 修改主键字段类型
假设订单表一开始使用数字类型,但后期希望改成可以包含字母的字符串类型,我们可以在迁移文件中填写:
```ts
import { Migration } from '@nocobase/server';
export default class MyMigration extends Migration {
async up() {
await this.sequelize.transaction(async transaction => {
await this.queryInterface.changeColumn('orders', 'id', {
type: DataTypes.STRING
}, {
transaction
});
});
}
}
```
注:修改字段类型只有在未加入新类型数据之前可以进行逆向降级操作,否则需要自行备份数据并对数据进行特定处理。
另外,直接修改数据表主键 `id` 的类型在某些数据库中会提示错误SQLite 正常PostgreSQL 失败)。这时候需要把相关操作分步执行:
1. 创建一个新的字符串类型字段 `id_new`
2. 复制原有表 `id` 的数据到新的字段;
3. 移除原有 `id` 主键约束;
4. 将原有 `id` 列改名为不使用的列名 `id_old`
5. 将新的 `id_new` 列改名为 `id`
6. 对新的 `id` 列增加主键约束;
```ts
import { Migration } from '@nocobase/server';
export default class MyMigration extends Migration {
async up() {
await this.sequelize.transaction(async transaction => {
await this.queryInterface.addColumn('orders', 'id_new', {
type: DataTypes.STRING
}, { transaction });
const PendingOrderModel = this.sequelize.define('orders', {
id_new: DataTypes.STRING
});
await PendingOrderModel.update({
id_new: col('id')
}, {
where: {
id: { [Op.not]: null }
},
transaction
});
await this.queryInterface.removeConstraint('orders', 'orders_pkey', { transaction });
await this.queryInterface.renameColumn('orders', 'id', 'id_old', { transaction });
await this.queryInterface.renameColumn('orders', 'id_new', 'id', { transaction });
await this.queryInterface.addConstraint('orders', {
type: 'PRIMARY KEY',
name: 'orders_pkey',
fields: ['id'],
transaction
});
});
}
}
```
通常修改列类型在已存在数据量较大的表里操作时也建议用新列代替旧列的方式,性能会更好。其他更多细节可以参考 [Sequelize 的 `queryInterface` API](https://sequelize.org/api/v6/class/src/dialects/abstract/query-interface.js),以及各个数据库引擎的细节。
注:在执行升级命令后,应用启动之前请确保变更的表结构能够对应上 collections 中定义的内容,以免不一致导致错误。

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -0,0 +1,462 @@
# 资源与操作
在 Web 开发领域,你可能听说过 RESTful 的概念NocoBase 也借用了这个资源的概念来映射系统中的各种实体,比如数据库中的数据、文件系统中的文件或某个服务等。但 NocoBase 基于实践考虑,并未完全遵循 RESTful 的约定,而是参考 [Google Cloud API 设计指南](https://cloud.google.com/apis/design) 的规范做了一些扩展,以适应更多的场景。
## 基础概念
与 RESTful 中资源的概念相同,是系统中对外提供的可操作的对象,可以是数据表、文件、和其他自定义的对象。
操作主要指对资源的读取和写入通常用于查阅数据、创建数据、更新数据、删除数据等。NocoBase 通过定义操作来实现对资源的访问,操作的核心其实是一个用于处理请求且兼容 Koa 的中间件函数。
### 数据表自动映射为资源
目前的资源主要针对数据库表中的数据NocoBase 在默认情况下都会将数据库中的数据表自动映射为资源,同时也提供了服务端的数据接口。所以在默认情况下,只要使用了 `db.collection()` 定义了数据表,就可以通过 NocoBase 的 HTTP API 访问到这个数据表的数据资源了。自动生成的资源的名称与数据库表定义的表名相同,比如 `db.collection({ name: 'users' })` 定义的数据表,对应的资源名称就是 `users`
同时,还为这些数据资源内置了常用的 CRUD 操作,对关系型数据资源也内置了关联数据从操作方法。
对简单数据资源的默认操作:
* [`list`](/api/actions#list):查询数据表中的数据列表
* [`get`](/api/actions#get):查询数据表中的单条数据
* [`create`](/api/actions#create):对数据表创建单条数据
* [`update`](/api/actions#update):对数据表更新单条数据
* [`destroy`](/api/actions#destroy):对数据表删除单条数据
对关系资源除了简单的 CRUD 操作,还有默认的关系操作:
* [`add`](/api/actions#add):对数据添加关联
* [`remove`](/api/actions#remove):对数据移除关联
* [`set`](/api/actions#set):对数据设置关联
* [`toggle`](/api/actions#toggle):对数据添加或移除关联
比如定义一个文章数据表并同步到数据:
```ts
app.db.collection({
name: 'posts',
fields: [
{ type: 'string', name: 'title' }
]
});
await app.db.sync();
```
之后针对 `posts` 数据资源的所有 CRUD 方法就可以直接通过 HTTP API 被调用了:
```bash
# create
curl -X POST -H "Content-Type: application/json" -d '{"title":"first"}' http://localhost:13000/api/posts:create
# list
curl http://localhost:13000/api/posts:list
# update
curl -X PUT -H "Content-Type: application/json" -d '{"title":"second"}' http://localhost:13000/api/posts:update
# destroy
curl -X DELETE http://localhost:13000/api/posts:destroy?filterByTk=1
```
### 自定义 Action
当默认提供的 CRUD 等操作不满足业务场景的情况下,也可以对特定资源扩展更多的操作。比如是对内置操作额外的处理需求,或者需要设置默认参数的情况。
针对特定资源的自定义操作,如覆盖定义文章表的创建操作:
```ts
// 等同于 app.resourcer.registerActions()
// 注册针对文章资源的 create 操作方法
app.actions({
async ['posts:create'](ctx, next) {
const postRepo = ctx.db.getRepository('posts');
await postRepo.create({
values: {
...ctx.action.params.values,
// 限定当前用户是文章的创建者
userId: ctx.state.currentUserId
}
});
await next();
}
});
```
这样在业务中就增加了合理的限制,用户不能以其他用户身份创建文章。
针对全局所有资源的自定义操作,如对所有数据表都增加导出的操作:
```ts
app.actions({
// 对所有资源都增加了 export 方法,用于导出数据
async export(ctx, next) {
const repo = ctx.db.getRepository(ctx.action.resource);
const results = await repo.find({
filter: ctx.action.params.filter
});
ctx.type = 'text/csv';
// 拼接为 CSV 格式
ctx.body = results
.map(row => Object.keys(row)
.reduce((arr, col) => [...arr, row[col]], []).join(',')
).join('\n');
next();
}
});
```
则可以按以下 HTTP API 的方式进行 CSV 格式的数据导出:
```bash
curl http://localhost:13000/api/<any_table>:export
```
### Action 参数
客户端的请求到达服务端后,相关的请求参数会被按规则解析并放在请求的 `ctx.action.params` 对象上。Action 参数主要有三个来源:
1. Action 定义时默认参数
2. 客户端请求携带
3. 其他中间件预处理
在真正操作处理函数处理之前,上面这三个部分的参数会按此顺序被合并到一起,最终传入操作的执行函数中。在多个中间件中也是如此,上一个中间件处理完的参数会被继续随 `ctx` 传递到下一个中间件中。
针对内置的操作可使用的参数,可以参考 [@nocobase/actions](/api/actions) 包的内容。除自定义操作以外,客户端请求主要使用这些参数,自定义的操作可以根据业务需求扩展需要的参数。
中间件预处理主要使用 `ctx.action.mergeParams()` 方法,且根据不同的参数类型有不同的合并策略,具体也可以参考 [mergeParams()](/api/resourcer/action#mergeparams) 方法的内容。
内置 Action 的默认参数在合并时只能以 `mergeParams()` 方法针对各个参数的默认策略执行,以达到服务端进行一定操作限制的目的。例如:
```ts
app.resource({
name: 'posts',
actions: {
create: {
whitelist: ['title', 'content'],
blacklist: ['createdAt', 'createdById'],
}
}
});
```
如上定义了针对 `posts` 资源的 `create` 操作,其中 `whitelist``blacklist` 分别是针对 `values` 参数的白名单和黑名单,即只允许 `values` 参数中的 `title``content` 字段,且禁止 `values` 参数中的 `createdAt``createdById` 字段。
### 自定义资源
数据型的资源还分为独立资源和关系资源:
* 独立资源:`<collection>`
* 关系资源:`<collection>.<association>`
```ts
// 等同于 app.resourcer.define()
// 定义文章资源
app.resource({
name: 'posts'
});
// 定义文章的作者资源
app.resource({
name: 'posts.user'
});
// 定义文章的评论资源
app.resource({
name: 'posts.coments'
});
```
需要自定义的情况主要针对于非数据库表类资源,比如内存中的数据、其他服务的代理接口等,以及需要对已有数据表类资源定义特定操作的情况。
例如定义一个与数据库无关的发送通知操作的资源:
```ts
app.resource({
name: 'notifications',
actions: {
async send(ctx, next) {
await someProvider.send(ctx.request.body);
next();
}
}
});
```
则在 HTTP API 中可以这样访问:
```bash
curl -X POST -d '{"title": "Hello", "to": "hello@nocobase.com"}' 'http://localhost:13000/api/notifications:send'
```
## 示例
我们继续之前 [数据表与字段示例](/development/guide/collections-fields#示例) 中的简单店铺场景,进一步理解资源与操作相关的概念。这里假设我们的在基于之前数据表的示例进行进一步资源和操作的定义,所以这里不再重复定义数据表的内容。
另外,只要定义了对应的数据表,我们对商品、订单等数据资源就可以直接使用这些默认操作以完成最场景的 CRUD 场景。
### 覆盖默认操作
某些情况下,不只是简单的针对单条数据的操作时,或者默认操作的参数需要有一定控制时,我们也可以覆盖默认的操作行为。比如我们创建订单时,不应该由客户端提交 `userId` 来代表订单的归属,而是应该由服务端根据当前登录用户来确定订单归属,这时我们就可以覆盖默认的 `create` 操作。对于简单的扩展,我们直接在插件的主类中编写:
```ts
import { Plugin } from '@nocobase/server';
import actions from '@nocobase/actions';
export default class ShopPlugin extends Plugin {
async load() {
// ...
this.app.resource({
name: 'orders',
actions: {
async create(ctx, next) {
ctx.action.mergeParams({
values: {
userId: ctx.state.user.id
}
});
return actions.create(ctx, next);
}
}
});
}
}
```
这样,我们在插件加载过程中针对订单数据资源就覆盖了默认的 `create` 操作,但在修改操作参数以后仍调用了默认逻辑,无需自行编写。修改提交参数的 `mergeParams()` 方法对内置默认操作来说非常有用,我们会在后面介绍。
### 数据表资源的自定义操作
当内置操作不能满足业务需求时,我们可以通过自定义操作来扩展资源的功能。例如通常一个订单会有很多状态,如果我们对 `status` 字段的取值设计为一系列枚举值:
* `-1`:已取消
* `0`:已下单,未付款
* `1`:已付款,未发货
* `2`:已发货,未签收
* `3`:已签收,订单完成
那么我们就可以通过自定义操作来实现订单状态的变更,比如对订单进行一个发货的操作,虽然简单的情况下可以通过 `update` 操作来实现,但是如果还有支付、签收等更复杂的情况,仅使用 `update` 会造成语义不清晰且参数混乱的问题,因此我们可以通过自定义操作来实现。
首先我们增加一张发货信息表的定义,保存到 `collections/deliveries.ts`
```ts
export default {
name: 'deliveries',
fields: [
{
type: 'belongsTo',
name: 'order'
},
{
type: 'string',
name: 'provider'
},
{
type: 'string',
name: 'trackingNumber'
},
{
type: 'integer',
name: 'status'
}
]
};
```
同时对订单表也扩展一个发货信息的关联字段(`collections/orders.ts`
```ts
export default {
name: 'orders',
fields: [
// ...other fields
{
type: 'hasOne',
name: 'delivery'
}
]
};
```
然后我们在插件的主类中增加对应的操作定义:
```ts
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
// ...
this.app.resource({
name: 'orders',
actions: {
async deliver(ctx, next) {
const { filterByTk } = ctx.action.params;
const orderRepo = ctx.db.getRepository('orders');
const [order] = await orderRepo.update({
filterByTk,
values: {
status: 2,
delivery: {
...ctx.action.params.values,
status: 0
}
}
});
ctx.body = order;
next();
}
}
});
}
}
```
其中Repository 是使用数据表数据仓库类,大部分进行数据读写的操作都会由此完成,详细可以参考 [Repository API](/api/database/repository) 部分。
定义好之后我们从客户端就可以通过 HTTP API 来调用“发货”这个操作了:
```bash
curl \
-X POST \
-H 'Content-Type: application/json' \
-d '{"provider": "SF", "trackingNumber": "SF1234567890"}' \
'/api/orders:deliver/<id>'
```
同样的,我们还可以定义更多类似的操作,比如支付、签收等。
### 参数合并
假设我们要提供用户查询自己的且只能查询自己的订单,同时我们需要限制用户不能查询已取消的订单,那么我们可以通过 action 的默认参数来定义:
```ts
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
// ...
this.app.resource({
name: 'orders',
actions: {
// 对 list 操作的默认参数
list: {
filter: {
// 由 users 插件扩展的过滤器运算符
$isCurrentUser: true,
status: {
$ne: -1
}
},
fields: ['id', 'status', 'createdAt', 'updatedAt']
}
}
});
}
}
```
当用户从客户端查询时,也可以在请求的 URL 上加入其他的参数,比如:
```bash
curl 'http://localhost:13000/api/orders:list?productId=1&fields=id,status,quantity,totalPrice&appends=product'
```
实际的查询条件会合并为:
```json
{
"filter": {
"$and": {
"$isCurrentUser": true,
"status": {
"$ne": -1
},
"productId": 1
}
},
"fields": ["id", "status", "quantity", "totalPrice", "createdAt", "updatedAt"],
"appends": ["product"]
}
```
并得到预期的查询结果。
另外的,如果我们需要对创建订单的接口限制不能由客户端提交订单编号(`id`)、总价(`totalPrice`)等字段,可以通过对 `create` 操作定义默认参数控制:
```ts
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
// ...
this.app.resource({
name: 'orders',
actions: {
create: {
blacklist: ['id', 'totalPrice', 'status', 'createdAt', 'updatedAt'],
values: {
status: 0
}
}
}
});
}
}
```
这样即使客户端故意提交了这些字段,也会被过滤掉,不会存在于 `ctx.action.params` 参数集中。
如果还要有更复杂的限制,比如只能在商品上架且有库存的情况下才能下单,可以通过配置中间件来实现:
```ts
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
// ...
this.app.resource({
name: 'orders',
actions: {
create: {
middlewares: [
async (ctx, next) => {
const { productId } = ctx.action.params.values;
const product = await ctx.db.getRepository('products').findOne({
filterByTk: productId,
filter: {
enabled: true,
inventory: {
$gt: 0
}
}
});
if (!product) {
return ctx.throw(404);
}
await next();
}
]
}
}
});
}
}
```
把部分业务逻辑(尤其是前置处理)放到中间件中,可以让我们的代码更加清晰,也更容易维护。
## 小结
通过上面的示例我们介绍了如何定义资源和相关的操作,回顾一下本章内容:
* 数据表自动映射为资源
* 内置默认的资源操作
* 对资源自定义操作
* 操作的参数合并顺序与策略
本章所涉及到的相关代码放到了一个完整的示例包 [packages/samples/shop-actions](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-actions) 中,可以直接在本地运行,查看效果。

View File

@ -0,0 +1,33 @@
# 配置中心
<img src="./settings-tab.jpg" style="max-width: 100%;"/>
## 示例
```tsx | pure
import { SettingsCenterProvider } from '@nocobase/client';
import React, { useContext } from 'react';
const HelloTab => () => <div>Hello Tab</div>;
export default React.memo((props) => {
return (
<SettingsCenterProvider
settings={{
'sample-hello': {
title: 'Hello',
icon: 'ApiOutlined',
tabs: {
tab1: {
title: 'Hello Tab',
component: HelloTab,
},
},
},
}}
>{props.children}</SettingsCenterProvider>
);
});
```
完整示例查看 [samples/hello](https://github.com/nocobase/nocobase/tree/develop/packages/samples/hello)。

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,63 @@
# UI 路由
NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `<RouteSwitch routes={[]} />` 来配置 ui routes例子如下
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { Link, MemoryRouter as Router } from 'react-router-dom';
import { RouteRedirectProps, RouteSwitchProvider, RouteSwitch } from '@nocobase/client';
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const routes: RouteRedirectProps[] = [
{
type: 'route',
path: '/',
exact: true,
component: 'Home',
},
{
type: 'route',
path: '/about',
component: 'About',
},
];
export default () => {
return (
<RouteSwitchProvider components={{ Home, About }}>
<Router initialEntries={['/']}>
<Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link>
<RouteSwitch routes={routes} />
</Router>
</RouteSwitchProvider>
);
};
```
在完整的 NocoBase 应用里,可以类似以下的的方式扩展 Route
```tsx | pure
import { RouteSwitchContext } from '@nocobase/client';
import React, { useContext } from 'react';
const HelloWorld = () => {
return <div>Hello ui router</div>;
};
export default React.memo((props) => {
const ctx = useContext(RouteSwitchContext);
ctx.routes.push({
type: 'route',
path: '/hello-world',
component: HelloWorld,
});
return <RouteSwitchContext.Provider value={ctx}>{props.children}</RouteSwitchContext.Provider>;
});
```
完整示例查看 [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page)

View File

@ -0,0 +1,2 @@
# 区块

View File

@ -0,0 +1,128 @@
# Collection Manager
```tsx
import React from 'react';
import { observer, ISchema, useForm } from '@formily/react';
import {
SchemaComponent,
SchemaComponentProvider,
Form,
Action,
CollectionProvider,
CollectionField,
} from '@nocobase/client';
import 'antd/dist/antd.css';
import { FormItem, Input } from '@formily/antd';
export default observer(() => {
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title1',
interface: 'input',
uiSchema: {
title: 'Title1',
type: 'string',
'x-component': 'Input',
required: true,
description: 'description1',
} as ISchema,
},
{
type: 'string',
name: 'title2',
interface: 'input',
uiSchema: {
title: 'Title2',
type: 'string',
'x-component': 'Input',
description: 'description',
default: 'ttt',
},
},
{
type: 'string',
name: 'title3',
},
],
};
const schema: ISchema = {
type: 'object',
properties: {
form1: {
type: 'void',
'x-component': 'Form',
properties: {
// 字段 title1 直接使用全局提供的 uiSchema
title1: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
default: '111',
},
// 等同于
// title1: {
// type: 'string',
// title: 'Title',
// required: true,
// 'x-component': 'Input',
// 'x-decorator': 'FormItem',
// },
title2: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
title: 'Title4', // 覆盖全局已定义的 Title2
required: true, // 扩展的配置参数
description: 'description4',
},
// 等同于
// title2: {
// type: 'string',
// title: 'Title22',
// required: true,
// 'x-component': 'Input',
// 'x-decorator': 'FormItem',
// },
// 字段 title3 没有提供 uiSchema自行处理
title3: {
'x-component': 'Input',
'x-decorator': 'FormItem',
title: 'Title3',
required: true,
},
action1: {
// type: 'void',
'x-component': 'Action',
title: 'Submit',
'x-component-props': {
type: 'primary',
useAction: '{{ useSubmit }}',
},
},
},
},
},
};
const useSubmit = () => {
const form = useForm();
return {
async run() {
form.submit(() => {
console.log(form.values);
});
},
};
};
return (
<SchemaComponentProvider scope={{ useSubmit }} components={{ Action, Form, CollectionField, Input, FormItem }}>
<CollectionProvider collection={collection}>
<SchemaComponent schema={schema} />
</CollectionProvider>
</SchemaComponentProvider>
);
});
```

View File

@ -0,0 +1,82 @@
# Schema 组件库
## 包装器组件
- BlockItem
- FormItem
- CardItem
## 布局
- Page
- Grid
- Tabs
- Space
## 字段组件
字段组件一般不单独使用,而是用在数据展示组件当中
- CollectionField万能组件
- Cascader
- Checkbox
- ColorSelect
- DatePicker
- Filter
- Formula
- IconPicker
- Input
- InputNumber
- Markdown
- Password
- Percent
- Radio
- RecordPicker
- RichText
- Select
- TimePicker
- TreeSelect
- Upload
## 数据展示组件
需要与字段组件搭配使用
- Calendar
- Form
- Kanban
- Table
- TableV2
## 操作onClick 事件型组件)
- Action
- Action.Drawer
- Action.Modal
- ActionBar用于操作布局
- Menu
## 其他
- G2plot
- Markdown.Void
## `x-designer``x-initializer` 的使用场景
`x-decorator``x-component` 是以下组件时,`x-designer` 生效:
- BlockItem
- CardItem
- FormItem
- Table.Column
- Tabs.TabPane
`x-decorator``x-component` 是以下组件时,`x-initializer` 生效:
- ActionBar
- BlockItem
- CardItem
- FormItem
- Grid
- Table
- Tabs

View File

@ -0,0 +1,107 @@
import { ArrayField } from '@formily/core';
import { connect, ISchema, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
const Value = connect((props) => {
return <li>value: {props.value}</li>;
});
const schema: ISchema = {
type: 'object',
properties: {
objArr: {
type: 'array',
default: [
{ __path: '0', id: 1, value: 't1' },
{
__path: '1',
id: 2,
value: 't2',
children: [
{
__path: '1.children.0',
id: 5,
value: 't5',
parentId: 2,
},
],
},
{
__path: '2',
id: 3,
value: 't3',
children: [
{
__path: '2.children.0',
id: 4,
value: 't4',
parentId: 3,
children: [
{
__path: '2.children.0.children.0',
id: 6,
value: 't6',
parentId: 4,
},
{
__path: '2.children.0.children.1',
id: 7,
value: 't7',
parentId: 4,
},
],
},
],
},
],
'x-component': 'ArrayTable',
'x-component-props': {
rowKey: 'id',
},
properties: {
c1: {
type: 'void',
'x-component': 'ArrayTable.Column',
properties: {
value: {
type: 'string',
'x-component': 'Value',
},
},
},
},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ ArrayTable, Value }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,353 @@
# Schema 的设计能力
Schema 的设计能力主要体现在
- 邻近位置插入,可用于
- 插入新的 schema 节点
- 现有 schema 节点的拖拽移动
- schema 参数修改
设计器核心 API 和参数有:
- 设计器 API`createDesignable()` & `useDesignable()`
- Schema 参数:`x-designer`,用于适配设计器组件
## 设计器 API
### createDesignable()
```ts
import { Schema } from '@nocobase/client';
const current = new Schema({
type: 'void',
'x-component': 'div',
});
const {
designable, // 是否可以配置
remove,
insertAdjacent, // 在某位置插入四个位置beforeBegin、afterBegin、beforeEnd、afterEnd
insertBeforeBegin, // 在当前节点的前面插入
insertAfterBegin, // 在当前节点的第一个子节点前面插入
insertBeforeEnd, // 在当前节点的最后一个子节点后面
insertAfterEnd, // 在当前节点的后面
} = createDesignable({
current,
});
const newSchema = {
type: 'void',
name: 'hello',
'x-component': 'Hello',
};
insertAfterBegin(newSchema);
console.log(current.toJSON());
{
type: 'void',
'x-component': 'div',
properties: {
hello: {
type: 'void',
'x-component': 'Hello',
},
},
}
```
### useDesignable()
React Hook 场景也可以用 `useDesignable()` 获取当前 schema 组件设计器的 API
```ts
const {
designable, // 是否可以配置
remove,
insertAdjacent, // 在某位置插入四个位置beforeBegin、afterBegin、beforeEnd、afterEnd
insertBeforeBegin, // 在当前节点的前面插入
insertAfterBegin, // 在当前节点的第一个子节点前面插入
insertBeforeEnd, // 在当前节点的最后一个子节点后面
insertAfterEnd, // 在当前节点的后面
} = useDesignable();
const schema = {
name: uid(),
'x-component': 'Hello',
};
// 在当前节点的前面插入
insertBeforeBegin(schema);
// 等同于
insertAdjacent('beforeBegin', schema);
// 在当前节点的第一个子节点前面插入
insertAfterBegin(schema);
// 等同于
insertAdjacent('afterBegin', schema);
// 在当前节点的最后一个子节点后面
insertBeforeEnd(schema);
// 等同于
insertAdjacent('beforeEnd', schema);
// 在当前节点的后面
insertAfterEnd(schema);
// 等同于
insertAdjacent('afterEnd', schema);
```
## 邻近位置插入
与 DOM 的 [insert adjacent](https://dom.spec.whatwg.org/#insert-adjacent) 概念相似Schema 也提供了 `insertAdjacent()` 方法用于解决邻近位置的插入问题。
四个邻近位置
```ts
{
properties: {
// beforeBegin 在当前节点的前面插入
node1: {
properties: {
// afterBegin 在当前节点的第一个子节点前面插入
// ...
// beforeEnd 在当前节点的最后一个子节点后面
},
},
// afterEnd 在当前节点的后面
},
}
```
和 HTML 标签一样Schema 组件库的组件也是可以相互组合,通过 insertAdjacent API 按实际需要插入在合理的邻近位置。
### 插入新的 schema 节点
在 Schema 组件里,可以直接通过 `useDesignable()` 在当前 Schema 的相邻位置插入新节点:
示例
```tsx
import React from 'react';
import { SchemaComponentProvider, SchemaComponent, useDesignable } from '@nocobase/client';
import { observer, Schema, useFieldSchema } from '@formily/react';
import { Button, Space } from 'antd';
import { uid } from '@formily/shared';
const Hello = observer((props) => {
const { insertAdjacent } = useDesignable();
const fieldSchema = useFieldSchema();
return (
<div>
<h1>{fieldSchema.name}</h1>
<Space>
<Button
onClick={() => {
insertAdjacent('beforeBegin', {
'x-component': 'Hello',
});
}}
>
before begin
</Button>
<Button
onClick={() => {
insertAdjacent('afterBegin', {
'x-component': 'Hello',
});
}}
>
after begin
</Button>
<Button
onClick={() => {
insertAdjacent('beforeEnd', {
'x-component': 'Hello',
});
}}
>
before end
</Button>
<Button
onClick={() => {
insertAdjacent('afterEnd', {
'x-component': 'Hello',
});
}}
>
after end
</Button>
</Space>
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
const Page = observer((props) => {
return <div>{props.children}</div>;
});
export default () => {
return (
<SchemaComponentProvider components={{ Page, Hello }}>
<SchemaComponent
schema={{
type: 'void',
name: 'page',
'x-component': 'Page',
properties: {
hello1: {
type: 'void',
'x-component': 'Hello',
},
},
}}
/>
</SchemaComponentProvider>
);
}
```
### 现有 schema 节点的拖拽移动
insertAdjacent 等方法也可用于节点的拖拽移动
```tsx
import React from 'react';
import { uid } from '@formily/shared';
import { observer, useField, useFieldSchema } from '@formily/react';
import { DndContext, DragEndEvent, useDraggable, useDroppable } from '@dnd-kit/core';
import { SchemaComponent, SchemaComponentProvider, createDesignable, useDesignable } from '@nocobase/client';
const useDragEnd = () => {
const { refresh } = useDesignable();
return ({ active, over }: DragEndEvent) => {
const activeSchema = active?.data?.current?.schema;
const overSchema = over?.data?.current?.schema;
if (!activeSchema || !overSchema) {
return;
}
const dn = createDesignable({
current: overSchema,
});
dn.on('insertAdjacent', refresh);
dn.insertBeforeBeginOrAfterEnd(activeSchema);
};
};
const Page = observer((props) => {
return <DndContext onDragEnd={useDragEnd()}>{props.children}</DndContext>;
});
function Draggable(props) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: props.id,
data: props.data,
});
const style = transform
? {
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
}
: undefined;
return (
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
{props.children}
</button>
);
}
function Droppable(props) {
const { isOver, setNodeRef } = useDroppable({
id: props.id,
data: props.data,
});
const style = {
color: isOver ? 'green' : undefined,
};
return (
<div ref={setNodeRef} style={style}>
{props.children}
</div>
);
}
const Block = observer((props) => {
const field = useField();
const fieldSchema = useFieldSchema();
return (
<Droppable id={field.address.toString()} data={{ schema: fieldSchema }}>
<div style={{ marginBottom: 20, padding: '20px', background: '#f1f1f1' }}>
Block {fieldSchema.name}{' '}
<Draggable id={field.address.toString()} data={{ schema: fieldSchema }}>
Drag
</Draggable>
</div>
</Droppable>
);
});
export default function App() {
return (
<SchemaComponentProvider components={{ Page, Block }}>
<SchemaComponent
schema={{
type: 'void',
name: 'page',
'x-component': 'Page',
properties: {
block1: {
'x-component': 'Block',
},
block2: {
'x-component': 'Block',
},
block3: {
'x-component': 'Block',
},
},
}}
/>
</SchemaComponentProvider>
);
}
```
## `x-designer` 的应用
`x-designer` 通常只在 BlockItem、CardItem、FormItem 等包装器组件中使用。
```ts
{
type: 'object',
properties: {
title: {
type: 'string',
title: '标题',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-designer': 'FormItem.Designer',
},
status: {
type: 'string',
title: '状态',
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-designer': 'FormItem.Designer',
},
},
}
```
说明NocoBase 提供的 Schema 设计器是以工具栏形式直接嵌入于界面,当激活界面配置时(`designable = true``x-designer` 组件(设计器工具栏)会显示出来,就可以通过工具栏更新当前 schema 组件了,工具栏提供的设计能力包括:
- 拖拽移动DndContext + DragHandler
- 插入新节点SchemaInitializer
- 参数配置SchemaSettings

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,518 @@
# 扩展 Schema 组件
除了原生的 html 标签,开发也可以适配更多的自定义组件,用于丰富 Schema 组件库。
扩展组件时,常用的方法:
- [connect](https://react.formilyjs.org/api/shared/connect) 无侵入接入第三方组件,一般用于适配字段组件,和 [mapProps](https://react.formilyjs.org/api/shared/map-props)[、mapReadPretty](https://react.formilyjs.org/api/shared/map-read-pretty) 搭配使用
- [observer](https://react.formilyjs.org/api/shared/observer) 当组件内部使用了 observable 对象,而你希望组件响应 observable 对象的变化时
## 最简单的扩展
直接将现成的 React 组件注册进来。
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const Hello = () => <h1>Hello, world!</h1>;
const schema = {
type: 'void',
name: 'hello',
'x-component': 'Hello',
};
export default () => {
return (
<SchemaComponentProvider components={{ Hello }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
## 通过 connect 接入第三方组件
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { Input } from 'antd'
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const ReadPretty = (props) => {
return <div>{props.value}</div>
};
const SingleText = connect(
Input,
mapProps((props, field) => {
return {
...props,
suffix: '后缀',
}
}),
mapReadPretty(ReadPretty),
);
const schema = {
type: 'object',
properties: {
t1: {
type: 'string',
default: 'hello t1',
'x-component': 'SingleText',
},
t2: {
type: 'string',
default: 'hello t2',
'x-component': 'SingleText',
'x-pattern': 'readPretty',
},
}
};
export default () => {
return (
<SchemaComponentProvider components={{ SingleText }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
## 使用 observer 响应数据
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { Input } from 'antd';
import { connect, observer, useForm } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const SingleText = connect(Input);
const UsedObserver = observer((props) => {
const form = useForm();
return <div>UsedObserver: {form.values.t1}</div>
});
const NotUsedObserver = (props) => {
const form = useForm();
return <div>NotUsedObserver: {form.values.t1}</div>
};
const schema = {
type: 'object',
properties: {
t1: {
type: 'string',
'x-component': 'SingleText',
},
t2: {
type: 'string',
'x-component': 'UsedObserver',
},
t3: {
type: 'string',
'x-component': 'NotUsedObserver',
},
}
};
const components = {
SingleText,
UsedObserver,
NotUsedObserver
};
export default () => {
return (
<SchemaComponentProvider components={components}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
## 嵌套的 Schema
- `props.children` 嵌套,适用于 void 和 object 类型的 properties例子见 [void 和 object 类型 schema 的嵌套](#void-和-object-类型-schema-的嵌套)
- `<RecursionField />` 自定义嵌套,所有类型都适用,例子见 [array 类型 schema 的嵌套](#array-类型-schema-的嵌套)
注意:
- 除了 void 和 object 类型以外的 schema 的 `properties` 无法直接通过 `props.children` 渲染,但是可以使用 `<RecursionField />` 解决嵌套问题
- 仅 void 和 object 类型的 schema 可以与 onlyRenderProperties 使用
```tsx | pure
<RecursionField schema={schema} onlyRenderProperties />
```
### void 和 object 类型 schema 的嵌套
直接通过 props.children 就可以适配 properties 节点了
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
// Hello 组件适配了 children可以嵌套 properties 了
const Hello = (props) => <h1>Hello, {props.children}!</h1>;
const World = () => <span>world</span>;
const schema = {
type: 'object',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ Hello, World }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
各类型 properties 渲染结果对比
```tsx
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const Hello = (props) => <h1>Hello, {props.children}!</h1>;
const World = () => <span>world</span>;
const schema = {
type: 'object',
properties: {
title1: {
type: 'void',
'x-content': 'Void schema渲染 properties',
},
void: {
type: 'void',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'void',
'x-component': 'World',
},
},
},
title2: {
type: 'void',
'x-content': 'Object schema渲染 properties',
},
object: {
type: 'object',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title3: {
type: 'void',
'x-content': 'Array schema不渲染 properties',
},
array: {
type: 'array',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title4: {
type: 'void',
'x-content': 'String schema不渲染 properties',
},
string: {
type: 'string',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
}
};
export default () => {
return (
<SchemaComponentProvider components={{ Hello, World }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
### array 类型 schema 的嵌套
可以通过 `<RecursionField />` 解决自定义嵌套问题
#### Array 元素是 string 或 number 时
```tsx
import React from 'react';
import { useFieldSchema, Schema, RecursionField, useField, observer, connect } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const useValueSchema = () => {
const schema = useFieldSchema();
return schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'Value') {
return s;
}
return buf;
});
};
const ArrayList = observer((props) => {
const field = useField();
const schema = useValueSchema();
return (
<>
String Array
<ul>
{field.value?.map((item, index) => {
// 只有一个元素
return <RecursionField name={index} schema={schema} />
})}
</ul>
</>
);
});
const Value = connect((props) => {
return <li>value: {props.value}</li>
});
const schema = {
type: 'object',
properties: {
strArr: {
type: 'array',
default: [1, 2, 3],
'x-component': 'ArrayList',
properties: {
value: {
type: 'number',
'x-component': 'Value',
},
}
},
}
};
export default () => {
return (
<SchemaComponentProvider components={{ ArrayList, Value }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
#### Array 元素是 Object 时
```tsx
import React from 'react';
import { useFieldSchema, Schema, RecursionField, useField, observer, connect } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const ArrayList = observer((props) => {
const field = useField();
const schema = useFieldSchema();
// array 类型的 schema 无法 onlyRenderProperties需要转化为 object 类型
const objSchema = new Schema({
type: 'object',
properties: schema.properties,
});
return (
<ul>
{field.value?.map((item, index) => {
// array 元素是 object
return (
<RecursionField name={index} schema={objSchema} onlyRenderProperties />
)
})}
</ul>
);
});
const Value = connect((props) => {
return <li>value: {props.value}</li>
});
const schema = {
type: 'object',
properties: {
objArr: {
type: 'array',
default: [
{ value: 't1' },
{ value: 't2' },
{ value: 't3' },
],
'x-component': 'ArrayList',
properties: {
value: {
type: 'number',
'x-component': 'Value',
},
}
}
}
};
export default () => {
return (
<SchemaComponentProvider components={{ ArrayList, Value }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
#### Tree 结构数据
```tsx
import { ArrayField } from '@formily/core';
import { connect, ISchema, observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import { Table, TableColumnType } from 'antd';
import React from 'react';
const ArrayTable = observer((props: any) => {
const { rowKey } = props;
const field = useField<ArrayField>();
const schema = useFieldSchema();
const columnSchemas = schema.reduceProperties((buf, s) => {
if (s['x-component'] === 'ArrayTable.Column') {
buf.push(s);
}
return buf;
}, []);
const columns = columnSchemas.map((s) => {
return {
render: (value, record) => {
return <RecursionField name={record.__path} schema={s} onlyRenderProperties />;
},
} as TableColumnType<any>;
});
return <Table rowKey={rowKey} columns={columns} dataSource={field.value} />;
});
const Value = connect((props) => {
return <li>value: {props.value}</li>;
});
const schema: ISchema = {
type: 'object',
properties: {
objArr: {
type: 'array',
default: [
{ __path: '0', id: 1, value: 't1' },
{
__path: '1',
id: 2,
value: 't2',
children: [
{
__path: '1.children.0',
id: 5,
value: 't5',
parentId: 2,
},
],
},
{
__path: '2',
id: 3,
value: 't3',
children: [
{
__path: '2.children.0',
id: 4,
value: 't4',
parentId: 3,
children: [
{
__path: '2.children.0.children.0',
id: 6,
value: 't6',
parentId: 4,
},
{
__path: '2.children.0.children.1',
id: 7,
value: 't7',
parentId: 4,
},
],
},
],
},
],
'x-component': 'ArrayTable',
'x-component-props': {
rowKey: 'id',
},
properties: {
c1: {
type: 'void',
'x-component': 'ArrayTable.Column',
properties: {
value: {
type: 'string',
'x-component': 'Value',
},
},
},
},
},
},
};
export default () => {
return (
<SchemaComponentProvider components={{ ArrayTable, Value }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```

View File

@ -0,0 +1,3 @@
# Overview
<code src="./demo1.tsx" />

View File

@ -0,0 +1,144 @@
# 邻近位置插入
与 DOM 的 [insert adjacent](https://dom.spec.whatwg.org/#insert-adjacent) 概念相似Schema 也提供了 `dn.insertAdjacent()` 方法用于解决邻近位置的插入问题。
## 邻近位置
```ts
{
properties: {
// beforeBegin 在当前节点的前面插入
node1: {
properties: {
// afterBegin 在当前节点的第一个子节点前面插入
// ...
// beforeEnd 在当前节点的最后一个子节点后面
},
},
// afterEnd 在当前节点的后面
},
}
```
## useDesignable()
获取当前 schema 节点设计器的 API
```ts
const {
designable, // 是否可以配置
insertAdjacent, // 在某位置插入四个位置beforeBegin、afterBegin、beforeEnd、afterEnd
insertBeforeBegin, // 在当前节点的前面插入
insertAfterBegin, // 在当前节点的第一个子节点前面插入
insertBeforeEnd, // 在当前节点的最后一个子节点后面
insertAfterEnd, // 在当前节点的后面
} = useDesignable();
const schema = {
name: uid(),
'x-component': 'Hello',
};
// 在当前节点的前面插入
insertBeforeBegin(schema);
// 等同于
insertAdjacent('beforeBegin', schema);
// 在当前节点的第一个子节点前面插入
insertAfterBegin(schema);
// 等同于
insertAdjacent('afterBegin', schema);
// 在当前节点的最后一个子节点后面
insertBeforeEnd(schema);
// 等同于
insertAdjacent('beforeEnd', schema);
// 在当前节点的后面
insertAfterEnd(schema);
// 等同于
insertAdjacent('afterEnd', schema);
```
示例
```tsx
import React from 'react';
import { SchemaComponentProvider, SchemaComponent, useDesignable } from '@nocobase/client';
import { observer, Schema, useFieldSchema } from '@formily/react';
import { Button, Space } from 'antd';
import { uid } from '@formily/shared';
const Hello = observer((props) => {
const { insertAdjacent } = useDesignable();
const fieldSchema = useFieldSchema();
return (
<div>
<h1>{fieldSchema.name}</h1>
<Space>
<Button
onClick={() => {
insertAdjacent('beforeBegin', {
'x-component': 'Hello',
});
}}
>
before begin
</Button>
<Button
onClick={() => {
insertAdjacent('afterBegin', {
'x-component': 'Hello',
});
}}
>
after begin
</Button>
<Button
onClick={() => {
insertAdjacent('beforeEnd', {
'x-component': 'Hello',
});
}}
>
before end
</Button>
<Button
onClick={() => {
insertAdjacent('afterEnd', {
'x-component': 'Hello',
});
}}
>
after end
</Button>
</Space>
<div style={{ margin: 50 }}>{props.children}</div>
</div>
);
});
const Page = observer((props) => {
return <div>{props.children}</div>;
});
export default () => {
return (
<SchemaComponentProvider components={{ Page, Hello }}>
<SchemaComponent
schema={{
type: 'void',
name: 'page',
'x-component': 'Page',
properties: {
hello1: {
type: 'void',
'x-component': 'Hello',
},
},
}}
/>
</SchemaComponentProvider>
);
}
```

View File

@ -0,0 +1,329 @@
# UI Schema 是什么?
一种描述前端组件的协议,基于 Formily Schema 2.0,类 JSON Schema 风格。
```ts
interface ISchema {
type: 'void' | 'string' | 'number' | 'object' | 'array';
name?: string;
title?: any;
// 包装器组件
['x-decorator']?: string;
// 包装器组件属性
['x-decorator-props']?: any;
// 组件
['x-component']?: string;
// 组件属性
['x-component-props']?: any;
// 展示状态,默认为 'visible'
['x-display']?: 'none' | 'hidden' | 'visible';
// 组件的子节点,简单使用
['x-content']?: any;
// children 节点 schema
properties?: Record<string, ISchema>;
// 以下仅字段组件时使用
// 字段联动
['x-reactions']?: SchemaReactions;
// 字段 UI 交互模式,默认为 'editable'
['x-pattern']?: 'editable' | 'disabled' | 'readPretty';
// 字段校验
['x-validator']?: Validator;
// 默认数据
default: ?:any;
// 设计器相关
// 设计器组件(工具栏),包括:拖拽移动、插入新节点、修改参数、移除等
['x-designer']?: any;
// 初始化器组件(工具栏),决定当前 schema 内部可以插入什么
['x-initializer']?: any;
}
```
## 最简单的组件
所有的原生 html 标签都可以转为 schema 的写法。如:
```ts
{
type: 'void',
'x-component': 'h1',
'x-content': 'Hello, world!',
}
```
JSX 示例
```tsx | pure
<h1>Hello, world!</h1>
```
## children 组件可以写在 properties 里
```ts
{
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
'x-component': 'input',
},
},
}
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
<input name={'title'} />
</div>
```
## decorator 的巧妙用法
decorator + component 的组合,可以将两个组件放在一个 schema 节点里,降低 schema 结构复杂度,提高组件的复用率。
例如表单场景里,可以将 FormItem 组件与任意字段组件组合,在这里 FormItem 就是 Decorator。
```ts
{
type: 'void',
['x-component']: 'div',
properties: {
title: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
content: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
}
```
JSX 等同于
```tsx | pure
<div>
<FormItem>
<Input name={'title'} />
</FormItem>
<FormItem>
<Input.TextArea name={'content'} />
</FormItem>
</div>
```
也可以提供一个 CardItem 组件,用于包裹所有区块,这样所有区块就都是 Card 包裹的了。
```ts
{
type: 'void',
['x-component']: 'div',
properties: {
title: {
type: 'string',
'x-decorator': 'CardItem',
'x-component': 'Table',
},
content: {
type: 'string',
'x-decorator': 'CardItem',
'x-component': 'Kanban',
},
},
}
```
JSX 等同于
```tsx | pure
<div>
<CardItem>
<Table />
</CardItem>
<CardItem>
<Kanban />
</CardItem>
</div>
```
## 组件的展示状态
- `'x-display': 'visible'`:显示组件
- `'x-display': 'hidden'`:隐藏组件,数据不隐藏
- `'x-display': 'none'`:隐藏组件,数据也隐藏
### `'x-display': 'visible'`
```ts
{
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
'x-component': 'input',
'x-display': 'visible'
},
},
}
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
<input name={'title'} />
</div>
```
### `'x-display': 'hidden'`
```ts
{
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
'x-component': 'input',
'x-display': 'hidden'
},
},
}
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
{/* 此处不输出 input 组件,对应的 name=title 的字段模型还存在 */}
</div>
```
### `'x-display': 'none'`
```ts
{
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
'x-component': 'input',
'x-display': 'none'
},
},
}
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
{/* 此处不输出 input 组件,对应的 name=title 的字段模型也不存在了 */}
</div>
```
## 组件的显示模式
用于字段组件,有三种显示模式:
- `'x-pattern': 'editable'` 可编辑
- `'x-pattern': 'disabled'` 不可编辑
- `'x-pattern': 'readPretty'` 友好阅读
如单行文本 `<SingleText />` 组件,编辑和不可编辑模式为 `<input />`,友好阅读模式为 `<div />`
### `'x-pattern': 'editable'`
```ts
const schema = {
name: 'test',
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
default: 'Hello',
'x-component': 'SingleText',
'x-pattern': 'editable'
},
},
};
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
<input name={'title'} value={'Hello'} />
</div>
```
### `'x-pattern': 'disabled'`
```ts
const schema = {
name: 'test',
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
default: 'Hello',
'x-component': 'SingleText',
'x-pattern': 'disabled'
},
},
};
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
<input name={'title'} value={'Hello'} disabled />
</div>
```
### `'x-pattern': 'readPretty'`
```ts
const schema = {
name: 'test',
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
default: 'Hello',
'x-component': 'SingleText',
'x-pattern': 'readPretty',
},
},
};
```
JSX 等同于
```tsx | pure
<div className={'form-item'}>
<div>Hello</div>
</div>
```

View File

@ -0,0 +1,61 @@
# x-designer 组件
## 内置 x-designer 组件
- Action.Designer
- Calendar.Designer
- Filter.Action.Designer
- Form.Designer
- FormItem.Designer
- FormV2.Designer
- FormV2.ReadPrettyDesigner
- DetailsDesigner
- G2Plot.Designer
- Kanban.Designer
- Kanban.Card.Designer
- Markdown.Void.Designer
- Menu.Designer
- TableV2.Column.Designer
- TableV2.ActionColumnDesigner
- TableBlockDesigner
- TableSelectorDesigner
- Tabs.Designer
## 替换
```tsx | pure
import React, { useContext } from 'react';
import { useFieldSchema } from '@formily/react';
import {
SchemaComponentOptions,
GeneralSchemaDesigner,
SchemaSettings,
useCollection
} from '@nocobase/client';
import React from 'react';
const CustomActionDesigner = () => {
const { name, title } = useCollection();
const fieldSchema = useFieldSchema();
return (
<GeneralSchemaDesigner title={title || name}>
<SchemaSettings.Remove
removeParentsIfNoChildren
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};
export default React.memo((props) => {
return (
<SchemaComponentOptions
components={{
'Action.Designer': CustomActionDesigner,
}}
>{props.children}</SchemaComponentOptions>
);
});
```

View File

@ -0,0 +1,39 @@
# x-initializer 组件
## 内置 x-initializer 组件
- BlockInitializers
- CalendarActionInitializers
- CreateFormBlockInitializers
- CustomFormItemInitializers
- DetailsActionInitializers
- FormActionInitializers
- FormItemInitializers
- KanbanActionInitializers
- ReadPrettyFormActionInitializers
- ReadPrettyFormItemInitializers
- RecordBlockInitializers
- RecordFormBlockInitializers
- SubTableActionInitializers
- TableActionColumnInitializers
- TableActionInitializers
- TableColumnInitializers
- TableSelectorInitializers
- TabPaneInitializers
## 替换
```tsx |pure
import React, { useContext } from 'react';
import { SchemaInitializerContext } from '@nocobase/client';
export default React.memo((props) => {
const items = useContext(SchemaInitializerContext);
const BlockInitializers = {};
return (
<SchemaInitializerContext.Provider value={{ ...items, BlockInitializers }}>
{props.children}
</SchemaInitializerContext.Provider>
);
});
```

View File

@ -4,7 +4,7 @@
---
Collection and Association resources are common.
Collection 和 Association 资源通用。
### `create`
@ -15,12 +15,12 @@ POST /api/users:create?whitelist=a,b&blacklist=c,d
```
- Parameters
- whitelist White list
- blacklist Black list
- Request body: JSON data to be inserted
- Response body data: Created data JSON
- whitelist 白名单
- blacklist 黑名单
- Request body: 待插入的 JSON 数据
- Response body data: 已创建的数据 JSON
#### Add a User
#### 新增用户
```bash
POST /api/users:create
@ -37,7 +37,7 @@ Response 200 (application/json)
}
```
#### Add a user's article
#### 新增用户文章
```bash
POST /api/users/1/posts:create
@ -49,11 +49,11 @@ Request Body
Response 200 (application/json)
{
"data": {},
"data": {}
}
```
#### Association in Request Body
#### Request Body 里的 association
```bash
POST /api/posts:create
@ -86,13 +86,13 @@ POST /api/users:create?filterByTk=1&whitelist=a,b&blacklist=c,d
```
- Parameters
- whitelist White list
- blacklist Black list
- filterByTk Filter by tk field, by default tk is the primary key of the data table
- filter Filtersupport json string
- Request body: JSON data to be updated
- whitelist 白名单
- blacklist 黑名单
- filterByTk 根据 tk 字段过滤,默认情况 tk 为数据表的主键
- filter 过滤,支持 json string
- Request body: 待更新的 JSON 数据
#### Association in Request Body
#### Request Body 里的 association
```bash
POST /api/posts:update/1
@ -137,6 +137,3 @@ Response 200 (application/json)
### `remove`
### `toggle`

View File

@ -1,6 +1,6 @@
# Filter operators
## Common
## 通用
- $eq
- $ne
@ -37,7 +37,7 @@
## boolean
- $isTruthy
- $isTruly
- $isFalsy
## date

View File

@ -1,36 +1,36 @@
# Overview
# 概述
NocoBase HTTP API is designed based on Resource & Action, it is a superset of REST API. The operation is not limited to add, delete, change, and check, Resource Action can be extended arbitrarily in NocoBase.
NocoBase 的 HTTP API 基于 Resource & Action 设计,是 REST API 的超集,操作不局限于增删改查,在 NocoBase 里Resource Action 可以任意的扩展。
## Resource
## 资源 Resource
Resource has two expressions in NocoBase.
在 NocoBase 里资源resource有两种表达方式
- `<collection>`
- `<collection>.<association>`
<Alert>
- collection is the set of all abstract data
- association is the association data for the collection
- resource includes both collection and collection.association
- collection 是所有抽象数据的集合
- association 为 collection 的关联数据
- resource 包括 collection 和 collection.association 两类
</Alert>
### Example
### 示例
- `posts` Post
- `posts.user` Post user
- `posts.tags` Post tags
- `posts` 文章
- `posts.user` 文章用户
- `posts.tags` 文章标签
## Action
## 操作 Action
Representing resource operations as `:<action>`
`:<action>` 的方式表示资源操作
- `<collection>:<action>`
- `<collection>.<association>:<action>`
Built-in global operations for collection or association
内置的全局操作,可用于 collection 或 association
- `create`
- `get`
@ -39,20 +39,20 @@ Built-in global operations for collection or association
- `destroy`
- `move`
Built-in association operation for association only
内置的关联操作,仅用于 association
- `set`
- `add`
- `remove`
- `toggle`
### Example
### 示例
- `posts:create` Create posts
- `posts.user:get` View posts user
- `posts.tags:add` Attach post tags (associate existing tags with post)
- `posts:create` 创建文章
- `posts.user:get` 查看文章用户
- `posts.tags:add` 附加文章标签(将现有的标签与文章关联)
## Request URL
## 请求 URL
```bash
<GET|POST> /api/<collection>:<action>
@ -61,9 +61,9 @@ Built-in association operation for association only
<GET|POST> /api/<collection>/<collectionIndex>/<association>:<action>/<associationIndex>
```
### Example
### 示例
posts resource
posts 资源
```bash
POST /api/posts:create
@ -73,7 +73,7 @@ POST /api/posts:update/1
POST /api/posts:destroy/1
```
posts.comments resource
posts.comments 资源
```bash
POST /api/posts/1/comments:create
@ -83,7 +83,7 @@ POST /api/posts/1/comments:update/1
POST /api/posts/1/comments:destroy/1
```
posts.tags resource
posts.tags 资源
```bash
POST /api/posts/1/tags:create
@ -95,45 +95,45 @@ POST /api/posts/1/tags:add
GET /api/posts/1/tags:remove
```
## Resource location
## 资源定位
- collection resource, locates the data to be processed by `collectionIndex`, `collectionIndex` must be unique
- association resource, locates the data to be processed by `collectionIndex` and `associationIndex` jointly, `associationIndex` may not be unique, but `collectionIndex` and `associationIndex`'s association indexes must be unique
- collection 资源,通过 `collectionIndex` 定位到待处理的数据,`collectionIndex` 必须唯一
- association 资源,通过 `collectionIndex``associationIndex` 联合定位待处理的数据,`associationIndex` 可能不是唯一的,但是 `collectionIndex``associationIndex` 的联合索引必须唯一
When viewing association resource details, the requested URL needs to provide both `<collectionIndex>` and `<associationIndex>`, `<collectionIndex>` is not redundant because `<associationIndex>` may not be unique.
查看 association 资源详情时,请求的 URL 需要同时提供 `<collectionIndex>``<associationIndex>``<collectionIndex>` 并不多余,因为 `<associationIndex>` 可能不是唯一的。
For example, `tables.fields` indicates the fields of a data table
例如 `tables.fields` 表示数据表的字段
```bash
GET /api/tables/table1/fields/title
GET /api/tables/table2/fields/title
```
Both table1 and table2 have a title field. The title is unique in table1, but other tables may also have a title field
table1 和 table2 都有 title 字段title 在 table1 里是唯一的,但是其他表也可能有 title 字段
## Request parameters
## 请求参数
Request parameters can be placed in the request's headers, parameters (query string), and body (GET requests do not have a body).
请求的参数可以放在 Request 的 headers、parametersquery string、bodyGET 请求没有 body 里。
A few special request parameters
几个特殊的 Parameters 请求参数
- `filter` Data filtering, used in query-related operations.
- `filterByTk` filter by tk field, used in operations that specify details of the data.
- `sort` Sorting, used in query-related operations.
- `fields` which data to output, for use in query-related operations.
- `appends` additional relationship fields for use in query-related operations.
- `except` which fields to exclude (no output), used in query-related operations.
- `whitelist` fields whitelist, used in data creation and update related operations.
- `blacklist` fields blacklist, used in data creation and update related operations.
- `filter` 数据过滤,用于查询相关操作里;
- `filterByTk` 根据 tk 字段字过滤,用于指定详情数据的操作里;
- `sort` 排序,用于查询相关操作里。
- `fields` 输出哪些数据,用于查询相关操作里;
- `appends` 附加关系字段,用于查询相关操作里;
- `except` 排除哪些字段(不输出),用于查询相关操作里;
- `whitelist` 字段白名单,用于数据的创建和更新相关操作里;
- `blacklist` 字段黑名单,用于数据的创建和更新相关操作里;
### filter
Data filter
数据过滤
```bash
# simple
GET /api/posts?filter[status]=publish
# Recommend using the json string format, which requires encodeURIComponent encoding
# 推荐使用 json string 的格式,需要 encodeURIComponent 编码
GET /api/posts?filter={"status":"published"}
# filter operators
@ -150,14 +150,14 @@ GET /api/posts?filter[user.email.$includes]=gmail
GET /api/posts?filter={"user.email.$includes":"gmail"}
```
[Click here for more information about filter operators](http-api/filter-operators)
[点此查看更多关于 filter operators 的内容](http-api/filter-operators)
### filterByTk
Filter by tk field. By default
根据 tk 字段过滤,默认情况:
- collection resource, tk is the primary key of the data table.
- association resource, tk is the targetKey field of the association.
- collection 资源tk 为数据表的主键;
- association 资源tk 为 association 的 targetKey 字段。
```bash
GET /api/posts:get?filterByTk=1&fields=name,title&appends=tags
@ -165,20 +165,20 @@ GET /api/posts:get?filterByTk=1&fields=name,title&appends=tags
### sort
Sorting. When sorting in descending order, the fields are preceded by the minus sign `-`.
排序。降序时,字段前面加上减号 `-`
```bash
# createAt field in ascending order
# createAt 字段升序
GET /api/posts:get?sort=createdAt
# createAt field descending
# createAt 字段降序
GET /api/posts:get?sort=-createdAt
# Multiple fields sorted jointly, createAt field descending, title A-Z ascending
# 多个字段联合排序createAt 字段降序、title A-Z 升序
GET /api/posts:get?sort=-createdAt,title
```
### fields
Which fields to output
输出哪些数据
```bash
GET /api/posts:list?fields=name,title
@ -197,47 +197,47 @@ Response 200 (application/json)
### appends
Appends a relationship field
附加关系字段
### except
Which fields to exclude (not output) for use in query-related operations.
排除哪些字段(不输出),用于查询相关操作里;
### whitelist
Whitelist
白名单
```bash
POST /api/posts:create?whitelist=title
{
"title": "My first post",
"date": "2022-05-19" # The date field will be filtered out and will not be written to the database
"date": "2022-05-19" # date 字段会被过滤掉,不会写入数据库
}
```
### blacklist
Blacklist
黑名单
```bash
POST /api/posts:create?blacklist=date
{
"title": "My first post",
"date": "2022-05-19" # The date field will be filtered out and will not be written to the database
"date": "2022-05-19" # date 字段会被过滤掉,不会写入数据库
}
```
## Request Response
## 请求响应
Format of the response
响应的格式
```ts
type ResponseResult = {
data?: any; // Master data
meta?: any; // Additional Data
errors?: ResponseError[]; // Errors
data?: any; // 主体数据
meta?: any; // 附加数据
errors?: ResponseError[]; // 报错
};
type ResponseError = {
@ -246,9 +246,9 @@ type ResponseError = {
};
```
### Example
### 示例
View list
查看列表
```bash
GET /api/posts:list
@ -270,7 +270,7 @@ Response 200 (application/json)
}
```
View details
查看详情
```bash
GET /api/posts:get/1
@ -280,11 +280,11 @@ Response 200 (application/json)
{
data: {
id: 1
}
},
}
```
Error
报错
```bash
POST /api/posts:create

View File

@ -1,12 +1,12 @@
# REST API
NocoBase's HTTP API is a superset of the REST API, and the standard CRUD API also supports the RESTful style.
NocoBase 的 HTTP API 是 REST API 的超集,标准的 CRUD API 也支持 RESTful 风格。
## Collection resources
## Collection 资源
---
### Create collection
### 创建 collection
HTTP API
@ -24,7 +24,7 @@ POST /api/<collection>
{} # JSON body
```
### List collection
### 查看 collection 列表
HTTP API
@ -38,7 +38,7 @@ REST API
GET /api/<collection>
```
### View collection details
### 查看 collection 详情
HTTP API
@ -53,7 +53,7 @@ REST API
GET /api/<collection>/<collectionIndex>
```
### Update collection
### 更新 collection
HTTP API
@ -62,7 +62,7 @@ POST /api/<collection>:update?filterByTk=<collectionIndex>
{} # JSON body
# Or
# 或者
POST /api/<collection>:update/<collectionIndex>
{} # JSON body
@ -76,13 +76,13 @@ PUT /api/<collection>/<collectionIndex>
{} # JSON body
```
### Delete collection
### 删除 collection
HTTP API
```bash
POST /api/<collection>:destroy?filterByTk=<collectionIndex>
# Or
# 或者
POST /api/<collection>:destroy/<collectionIndex>
```
@ -92,11 +92,11 @@ REST API
DELETE /api/<collection>/<collectionIndex>
```
## Association resources
## Association 资源
---
### Create Association
### 创建 Association
HTTP API
@ -114,7 +114,7 @@ POST /api/<collection>/<collectionIndex>/<association>
{} # JSON body
```
### List Association
### 查看 Association 列表
HTTP API
@ -128,13 +128,13 @@ REST API
GET /api/<collection>/<collectionIndex>/<association>
```
### View Association details
### 查看 Association 详情
HTTP API
```bash
GET /api/<collection>/<collectionIndex>/<association>:get?filterByTk=<associationIndex>
# Or
# 或者
GET /api/<collection>/<collectionIndex>/<association>:get/<associationIndex>
```
@ -144,7 +144,7 @@ REST API
GET /api/<collection>/<collectionIndex>/<association>:get/<associationIndex>
```
### Update Association
### 更新 Association
HTTP API
@ -153,7 +153,7 @@ POST /api/<collection>/<collectionIndex>/<association>:update?filterByTk=<asso
{} # JSON body
# Or
# 或者
POST /api/<collection>/<collectionIndex>/<association>:update/<associationIndex>
{} # JSON body
@ -164,16 +164,16 @@ REST API
```bash
PUT /api/<collection>/<collectionIndex>/<association>:update/<associationIndex>
{} # JSON
{} # JSON 数据
```
### Delete Association
### 删除 Association
HTTP API
```bash
POST /api/<collection>/<collectionIndex>/<association>:destroy?filterByTk=<associationIndex>
# Or
# 或者
POST /api/<collection>/<collectionIndex>/<association>:destroy/<associationIndex>
```

View File

@ -0,0 +1,42 @@
# 介绍
NocoBase 采用微内核架构,各类功能以插件形式扩展,所以微内核架构也叫插件化架构,由内核和插件两部分组成。内核提供了最小功能的 WEB 服务器,还提供了各种插件化接口;插件是按功能划分的各种独立模块,通过接口适配,具有可插拔的特点。插件化的设计降低了模块之间的耦合度,提高了复用率。随着插件库的不断扩充,常见的场景只需要组合插件即可完成基础搭建。例如 NocoBase 的无代码平台,就是由各种插件组合起来。
<img src="./guide/pm-built-in.jpg"/>
## 插件管理器
NocoBase 提供了强大的插件管理器用于管理插件,插件管理器的流程如下:
<img src="./pm-flow.svg"/>
开发可以通过 CLI 的方式管理插件:
```bash
# 创建插件
yarn pm create hello
# 注册插件
yarn pm add hello
# 激活插件
yarn pm enable hello
# 禁用插件
yarn pm disable hello
# 删除插件
yarn pm remove hello
```
无代码用户也可以通过插件管理器界面激活、禁用、删除已添加的本地插件:
<img src="./pm-ui.jpg"/>
更多插件示例,查看 [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples)。
## 扩展能力
无论是通用性的功能还是个性化定制都建议以插件的形式编写NocoBase 的扩展性体现在方方面面:
- 可以是用户直观可见的界面相关的页面模块、区块类型、操作类型、字段类型等
- 也可以是用于增强或限制 HTTP API 的过滤器、校验器、访问限制等
- 也可以是更底层的数据表、迁移、事件、命令行等功能的增强
不仅如此,更多扩展介绍请查看 [扩展指南 - 概述](/development/guide) 章节。

View File

@ -4,18 +4,18 @@ order: 2
# NocoBase CLI
NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications.
NocoBase CLI 旨在帮助你开发、构建和部署 NocoBase 应用。
<Alert>
NocoBase CLI supports both ts-node and node modes
NocoBase CLI 支持 ts-node 和 node 两种运行模式
- ts-node mode (default): used for development environment, supports real-time compilation, but slow response
- node mode: for production environment, fast response, but need to execute `yarn nocobase build` to compile all source code first
- ts-node 模式(默认):用于开发环境,支持实时编译,但是响应较慢
- node 模式:用于生产环境,响应迅速,但需要先执行 `yarn nocobase build` 将全部源码进行编译
</Alert>
## Instructions
## 使用说明
```bash
$ yarn nocobase -h
@ -26,25 +26,25 @@ Options:
-h, --help
Commands:
create-plugin Create plugin scaffolding
create-plugin 创建插件脚手架
console
db:auth Verify that the database connection is successful
db:sync Generate relevant data tables and fields from collections configuration
install Install
start Start the application in the production environment
build Compile and package
clean Delete the compiled files
dev Start the application for the development environment and supports live compilation
doc Documentation development
test Test
db:auth 校验数据库是否连接成功
db:sync 通过 collections 配置生成相关数据表和字段
install 安装
start 生产环境启动应用
build 编译打包
clean 删除编译之后的文件
dev 启动应用,用于开发环境,支持实时编译
doc 文档开发
test 测试
umi
upgrade Upgrade
upgrade 升级
help
```
## Use in scaffolding
## 在脚手架里应用
The `scripts` in the application scaffolding `package.json` are as follows
应用脚手架 `package.json` 里的 `scripts` 如下:
```json
{
@ -59,9 +59,9 @@ The `scripts` in the application scaffolding `package.json` are as follows
}
```
## Extensions
## 命令行扩展
NocoBase CLI is built on [commander](https://github.com/tj/commander.js), you can freely extend the command, the extended command can be written in `app/server/index.ts`.
NocoBase CLI 基于 [commander](https://github.com/tj/commander.js) 构建,你可以自由扩展命令,扩展的 command 可以写在 `app/server/index.ts` 里:
```ts
const app = new Application(config);
@ -69,7 +69,7 @@ const app = new Application(config);
app.command('hello').action(() => {});
```
Alternatively, write in the plugin.
或者,写在插件里:
```ts
class MyPlugin extends Plugin {
@ -79,22 +79,22 @@ class MyPlugin extends Plugin {
}
```
Terminal runs
终端运行
```bash
$ yarn nocobase hello
```
## Built-in command line
## 内置命令行
Sort by frequency of use
按使用频率排序
### `dev`
Start the application in the development environment and the code is compiled in real time.
开发环境下,启动应用,代码实时编译。
<Alert>
NocoBase will be installed automatically if it is not installed (refer to the install command)
NocoBase 未安装时,会自动安装(参考 install 命令)
</Alert>
```bash
@ -107,25 +107,25 @@ Options:
-h, --help
```
Example
示例
```bash
# Start application for development environment, live compile
# 启动应用,用于开发环境,实时编译
yarn nocobase dev
# Start only the server side
# 只启动服务端
yarn nocobase dev --server
# Start only the client side
# 只启动客户端
yarn nocobase dev --client
```
### `start`
Start the application in a production environment, the code needs to be yarn build.
生产环境下,启动应用,代码需要 yarn build。
<Alert>
- NocoBase will be installed automatically if it is not installed (refer to the install command)
- If the source code has been modified, it needs to be repackaged (refer to the build command)
- NocoBase 未安装时,会自动安装(参考 install 命令)
- 源码有修改时,需要重新打包(参考 build 命令)
</Alert>
@ -140,16 +140,16 @@ Options:
-h, --help
```
Example
示例
```bash
# Start the application in a production environment
# 启动应用,用于生产环境,
yarn nocobase start
```
### `install`
Install
安装
```bash
$ yarn nocobase install -h
@ -167,28 +167,28 @@ Options:
-h, --help
```
Example
示例
```bash
# Initial Installation
yarn nocobase install -l en-US -e admin@nocobase.com -p admin123
# Delete all data tables of NocoBase and reinstall
yarn nocobase install -f -l en-US -e admin@nocobase.com -p admin123
# Empty the database and reinstall
yarn nocobase install -c -l en-US -e admin@nocobase.com -p admin123
# 初始安装
yarn nocobase install -l zh-CN -e admin@nocobase.com -p admin123
# 删除 NocoBase 的所有数据表,并重新安装
yarn nocobase install -f -l zh-CN -e admin@nocobase.com -p admin123
# 清空数据库,并重新安装
yarn nocobase install -c -l zh-CN -e admin@nocobase.com -p admin123
```
<Alert>
Difference between `-f/--force` and `-c/--clean`
- `-f/--force` Delete all data tables of NocoBase
- `-c/--clean` Delete all data tables of the database
`-f/--force``-c/--clean` 的区别
- `-f/--force` 删除 NocoBase 的数据表
- `-c/--clean` 清空数据库,所有数据表都会被删除
</Alert>
### `upgrade`
Upgrade
升级
```bash
yarn nocobase upgrade
@ -196,7 +196,7 @@ yarn nocobase upgrade
### `test`
jest tests, supports all [jest-cli](https://jestjs.io/docs/cli) options, and extends `-c, --db-clean` support in addition.
jest 测试,支持所有 [jest-cli](https://jestjs.io/docs/cli) 的 options除此之外还扩展了 `-c, --db-clean` 的支持。
```bash
$ yarn nocobase test -h
@ -204,62 +204,62 @@ $ yarn nocobase test -h
Usage: nocobase test [options]
Options:
-c, --db-clean Empty the database before running all tests
-c, --db-clean 运行所有测试前清空数据库
-h, --help
```
Example
示例
```bash
# Run all test files
# 运行所有测试文件
yarn nocobase test
# Run all test files in the specified folder
# 运行指定文件夹下所有测试文件
yarn nocobase test packages/core/server
# Run all tests in the specified file
# 运行指定文件里的所有测试
yarn nocobase test packages/core/database/src/__tests__/database.test.ts
# Empty the database before running tests
# 运行测试前,清空数据库
yarn nocobase test -c
yarn nocobase test packages/core/server -c
```
### `build`
Before deployed to the production environment, the source code needs to be compiled and packaged. It needs to be rebuilt if there are changes to the code.
代码部署到生产环境前,需要将源码编译打包,如果代码有修改,也需要重新构建。
```bash
# All packages
# 所有包
yarn nocobase build
# Specified package
# 指定包
yarn nocobase build app/server app/client
```
### `clean`
Delete the compiled file
删除编译之后的文件
```bash
yarn clean
# Equivalent to
# 等同于
yarn rimraf -rf packages/*/*/{lib,esm,es,dist}
```
### `doc`
Documentation development
文档开发
```bash
# Start documentation
yarn doc --lang=en-US # Equivalent to yarn doc dev
# Build the documentation and output it to . /docs/dist/ directory by default
# 启动文档
yarn doc --lang=zh-CN # 等同于 yarn doc dev
# 构建文档,默认输出到 ./docs/dist/ 目录下
yarn doc build
# View the final result of the document output by dist
yarn doc serve --lang=en-US
# 查看 dist 输出的文档最终效果
yarn doc serve --lang=zh-CN
```
### `db:auth`
Verify that the database is successfully connected
校验数据库是否连接成功
```bash
$ yarn nocobase db:auth -h
@ -267,13 +267,13 @@ $ yarn nocobase db:auth -h
Usage: nocobase db:auth [options]
Options:
-r, --retry [retry] retry times
-r, --retry [retry] 重试次数
-h, --help
```
### `db:sync`
Generate data tables and fields via collections configuration
通过 collections 配置生成数据表和字段
```bash
$ yarn nocobase db:sync -h
@ -287,24 +287,24 @@ Options:
### `umi`
`app/client` is built based on [umi](https://umijs.org/) and can be used to execute other related commands via `nocobase umi`.
`app/client` 基于 [umi](https://umijs.org/) 构建,可以通过 `nocobase umi` 来执行其他相关命令。
```bash
# Generate the .umi cache required by the development environment
# 生成开发环境所需的 .umi 缓存
yarn nocobase umi generate tmp
```
### `help`
The help command, also available with the option parameter, `-h` and `--help`
帮助命令,也可以用 option 参数,`-h` 和 `--help`
```bash
# View all cli
# 查看所有 cli
yarn nocobase help
# You can also use -h
# 也可以用 -h
yarn nocobase -h
# or --help
# 或者 --help
yarn nocobase --help
# Option to view the db:sync command
# 查看 db:sync 命令的 option
yarn nocobase db:sync -h
```

View File

@ -0,0 +1 @@
# Building

View File

@ -0,0 +1,163 @@
# 单元测试
## 介绍
NocoBase 的测试基于 [Jest](https://jestjs.io/) 测试框架。同时,为了方便的编写测试,我们提供了两个工具类,在测试环境模拟正常的数据库和应用的服务端。
### MockDatabase
模拟数据库类继承自 [`Database`](/api/database) 类,大部分内容没有区别,主要在构造函数默认内置了随机表前缀,在每个测试用例初始化数据库时相关数据表都通过前缀名称与其他用例进行隔离,在运行测试用例时互不影响。
```ts
import { MockDatabase } from '@nocobase/test';
describe('my suite', () => {
let db;
beforeEach(async () => {
db = new MockDatabase();
db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
await db.sync();
});
test('my case', async () => {
const postRepository = db.getRepository('posts');
const p1 = await postRepository.create({
values: {
title: 'hello'
}
});
expect(p1.get('title')).toEqual('hello');
});
});
```
### MockServer
模拟服务器也继承自 [Application](/api/server/application) 类,除了内置的数据库实例是通过模拟数据库类生成的以外,还提供了比较方便的生成基于 [superagent](https://www.npmjs.com/package/superagent) 请求代理功能,针对从发送请求到获取响应的写法也集成了 `.resource('posts').create()`,比较简化。
```ts
import { mockServer } from '@nocobase/test';
describe('my suite', () => {
let app;
let agent;
let db;
beforeEach(async () => {
app = mockServer();
agent = app.agent();
db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
await db.sync();
await app.load();
});
test('my case', async () => {
const { body } = await agent.resource('posts').create({
values: {
title: 'hello'
}
});
expect(body.data.title).toEqual('hello');
});
});
```
## 示例
我们以之前在 [资源与操作](development/guide/resources-actions) 章节的功能为例,来写一个插件的测试:
```ts
import { mockServer } from '@nocobase/test';
import Plugin from '../../src/server';
describe('shop actions', () => {
let app;
let agent;
let db;
beforeEach(async () => {
app = mockServer();
app.plugin(Plugin);
agent = app.agent();
db = app.db;
await app.load();
await db.sync();
});
afterEach(async () => {
await app.destroy();
});
test('product order case', async () => {
const { body: product } = await agent.resource('products').create({
values: {
title: 'iPhone 14 Pro',
price: 7999,
enabled: true,
inventory: 1
}
});
expect(product.data.price).toEqual(7999);
const { body: order } = await agent.resource('orders').create({
values: {
productId: product.data.id
}
});
expect(order.data.totalPrice).toEqual(7999);
expect(order.data.status).toEqual(0);
const { body: deliveredOrder } = await agent.resource('orders').deliver({
filterByTk: order.data.id,
values: {
provider: 'SF',
trackingNumber: '123456789'
}
});
expect(deliveredOrder.data.status).toBe(2);
expect(deliveredOrder.data.delivery.trackingNumber).toBe('123456789');
});
});
```
编写完成后,在命令行中允许测试命令:
```bash
yarn test packages/samples/shop-actions
```
该测试将验证:
1. 商品可以创建成功;
2. 订单可以创建成功;
3. 订单可以发货成功;
当然这只是个最基本的例子,从业务上来说并不完善,但作为示例已经可以说明整个测试的流程。
## 小结
本章涉及的示例代码整合在对应的包 [packages/samples/shop-actions](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-actions) 中,可以直接在本地运行,查看效果。

View File

@ -1 +0,0 @@
# Quick Start

View File

@ -1 +0,0 @@
# PluginManager

View File

@ -1 +0,0 @@
# Resource & Action

Some files were not shown because too many files have changed in this diff Show More