refactor(doc): change to new structure (#804)

* refactor(doc): change to new structure

* docs: add database docs

* docs: add collection docs

* docs: add db field examples

* docs(api): fix filename and menu path

* docs: add database docs

* docs: add db operators doc

* docs: add resourcer menu

* docs: add resourcer docs

* docs: fix api docs

* docs: refactor api menu structure

* feat: update docs (#830)

* feat: updates

* feat: update docs

* chore: ignore docs from ci

Co-authored-by: Junyi <mytharcher@users.noreply.github.com>
Co-authored-by: mytharcher <mytharcher@gmail.com>

* docs: add database methods docs

* docs: add missed api

* docs: fix api docs

* feat: update development docs (#833)

* feat: update development docs

* feat: update docs

* feat: update docs

* docs: add first plugin example (#834)

* feat: update docs

* feat: update docs

* docs: fix typo

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2022-09-19 09:23:01 +08:00 committed by GitHub
parent f9f8dc78f4
commit 796e73ae5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
136 changed files with 6152 additions and 628 deletions

View File

@ -1,6 +1,9 @@
name: Nocobase test name: Nocobase test
on: [push, pull_request] on:
pull_request:
paths-ignore:
- 'docs/**'
jobs: jobs:
test: test:

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ lerna-debug.log
*.sqlite *.sqlite
coverage coverage
.umi .umi
.umi-production
/uploads /uploads
.env.test .env.test
docs-dist/ docs-dist/

View File

@ -42,8 +42,8 @@ const getTitle = (item, lang) => {
return item.title; return item.title;
}; };
const parseMenuItems = (items, lang = null) => { const parseMenuItems = (items: any[], lang: string) => {
const menuItems = []; const menuItems: any[] = [];
for (const item of items) { for (const item of items) {
if (typeof item === 'string') { if (typeof item === 'string') {
const result = markdown(item, lang); const result = markdown(item, lang);
@ -86,18 +86,63 @@ export default defineConfig({
locales: [[lang, lang]], locales: [[lang, lang]],
hash: true, hash: true,
logo: 'https://www.nocobase.com/images/logo.png', logo: 'https://www.nocobase.com/images/logo.png',
navs: [ navs: {
{ 'en-US': [
title: 'Docs', {
path: '/', title: 'Introduction',
hidden: true, path: '/introduction'
}, },
{ {
title: 'GitHub', title: 'Getting started',
path: 'https://github.com/nocobase/nocobase', path: '/getting-started'
}, },
], {
menus: { title: 'Manual',
'/': parseMenuItems(menus, lang), path: '/manual'
},
{
title: 'Development',
path: '/development'
},
{
title: 'API reference',
path: '/api'
},
{
title: 'GitHub',
path: 'https://github.com/nocobase/nocobase',
},
],
'zh-CN': [
{
title: '介绍',
path: '/introduction'
},
{
title: '快速开始',
path: '/getting-started'
},
{
title: '使用手册',
path: '/manual'
},
{
title: '开发教程',
path: '/development'
},
{
title: 'API 参考',
path: '/api'
},
{
title: 'GitHub',
path: 'https://github.com/nocobase/nocobase',
},
]
}, },
menus: Object.keys(menus).reduce((result, key) => {
const items = menus[key];
result[key] = parseMenuItems(items, lang);
return result;
}, {})
}); });

View File

@ -1,140 +1,240 @@
export default [ export default {
{ '/introduction': [
title: 'Welcome', '/introduction/index',
'title.zh-CN': '欢迎', '/introduction/features',
type: 'group', '/introduction/when',
children: [ '/introduction/quickstart',
'/index', '/introduction/roadmap',
'/why', '/introduction/thanks',
'/who', ],
'/release-notes', '/getting-started': [
], {
}, title: 'Installation',
{ 'title.zh-CN': '安装',
title: 'Getting started', type: 'subMenu',
'title.zh-CN': '快速开始', children: [
type: 'group', '/getting-started/installation/index',
children: [ '/getting-started/installation/docker-compose',
{ '/getting-started/installation/create-nocobase-app',
title: 'Installation', '/getting-started/installation/git-clone',
'title.zh-CN': '安装', ],
type: 'subMenu', },
children: [ '/getting-started/upgrading',
'/getting-started/installation/overview', '/getting-started/deployment',
'/getting-started/installation/docker-compose', ],
'/getting-started/installation/create-nocobase-app', '/manual': [
'/getting-started/installation/git-clone', '/manual/functional-zoning',
], '/manual/collections',
}, '/manual/menus',
'/getting-started/upgrading', '/manual/blocks',
], '/manual/actions',
}, '/manual/roles-permissions',
{ '/manual/tabs',
title: 'User manual', '/manual/file-storages',
'title.zh-CN': '用户手册', '/manual/system-settings',
type: 'group', '/manual/plugins',
children: [ ],
'/user-manual/introduction/5-minutes-to-get-started', '/development': [
{ {
title: 'Advanced guide', title: 'Getting started',
'title.zh-CN': '深入 NocoBase', 'title.zh-CN': '快速开始',
type: 'subMenu', type: 'group',
children: [ children: [
'/user-manual/advanced-guide/functional-zoning', '/development/index',
'/user-manual/advanced-guide/collections', '/development/your-fisrt-plugin'
'/user-manual/advanced-guide/menus', ],
'/user-manual/advanced-guide/blocks', },
'/user-manual/advanced-guide/actions', {
'/user-manual/advanced-guide/roles-permissions', title: 'Extension Guides',
'/user-manual/advanced-guide/tabs', 'title.zh-CN': '扩展指南',
'/user-manual/advanced-guide/file-storages', type: 'group',
'/user-manual/advanced-guide/system-settings', children: [
'/user-manual/advanced-guide/plugins', '/development/guide/index',
], '/development/guide/collections-fields',
}, '/development/guide/commands',
], '/development/guide/hooks',
}, '/development/guide/i18n',
{ '/development/guide/middleware',
title: 'Development', '/development/guide/migration',
'title.zh-CN': '开发指南', '/development/guide/resources-actions',
type: 'group', {
children: [ title: 'UI Schema Designer',
'/development/directory-structure', type: 'subMenu',
'/development/env', children: [
'/development/nocobase-cli', // '/development/guide/ui-schema-designer/index',
{ '/development/guide/ui-schema-designer/what-is-ui-schema',
title: 'HTTP API', '/development/guide/ui-schema-designer/extending-schema-components',
'title.zh-CN': 'HTTP API', // '/development/guide/ui-schema-designer/insert-adjacent',
type: 'subMenu', '/development/guide/ui-schema-designer/designable',
children: [ '/development/guide/ui-schema-designer/component-library',
'/development/http-api/index', '/development/guide/ui-schema-designer/collection-manager',
'/development/http-api/rest-api', '/development/guide/ui-schema-designer/acl',
'/development/http-api/action-api', ],
'/development/http-api/javascript-sdk', },
'/development/http-api/filter-operators', '/development/guide/ui-router',
], ],
}, },
'/development/javascript-sdk', {
{ title: 'Test & Build',
title: 'Plugin development', 'title.zh-CN': '测试与构建',
'title.zh-CN': '插件开发', type: 'group',
type: 'subMenu', children: ['/development/pre-release/test', '/development/pre-release/build'],
children: [ },
'/development/plugin-development/index', {
{ title: 'HTTP API',
title: 'Server', type: 'group',
'title.zh-CN': 'Server', children: [
type: 'subMenu', '/api/http-api/index',
children: [ '/api/http-api/rest-api',
'/development/plugin-development/server/overview', ],
'/development/plugin-development/server/database', },
'/development/plugin-development/server/resourcer', ],
'/development/plugin-development/server/middleware', // {
'/development/plugin-development/server/acl', // title: 'Development',
'/development/plugin-development/server/events', // 'title.zh-CN': '开发指南',
'/development/plugin-development/server/i18n', // type: 'group',
'/development/plugin-development/server/cli', // children: [
'/development/plugin-development/server/app-manager', // '/development/directory-structure',
'/development/plugin-development/server/plugin-manager', // '/development/env',
], // '/development/nocobase-cli',
}, // {
{ // title: 'HTTP API',
title: 'Client', // 'title.zh-CN': 'HTTP API',
'title.zh-CN': 'Client', // type: 'subMenu',
type: 'subMenu', // children: [
children: [ // '/development/http-api/index',
'/development/plugin-development/client/overview', // '/development/http-api/rest-api',
{ // '/development/http-api/action-api',
title: 'Providers', // '/development/http-api/javascript-sdk',
'title.zh-CN': 'Providers', // '/development/http-api/filter-operators',
type: 'subMenu', // ],
children: [ // },
'/development/plugin-development/client/providers/acl', // '/development/javascript-sdk',
'/development/plugin-development/client/providers/antd', // {
'/development/plugin-development/client/providers/api-client', // title: 'Plugin development',
'/development/plugin-development/client/providers/collection-manager', // 'title.zh-CN': '插件开发',
'/development/plugin-development/client/providers/i18n', // type: 'subMenu',
'/development/plugin-development/client/providers/route-switch', // children: [
'/development/plugin-development/client/providers/schema-component', // '/development/plugin-development/index',
'/development/plugin-development/client/providers/schema-initializer', // {
], // title: 'Server',
}, // 'title.zh-CN': 'Server',
], // type: 'subMenu',
}, // children: [
], // '/development/plugin-development/server/overview',
}, // '/development/plugin-development/server/database',
], // '/development/plugin-development/server/resourcer',
}, // '/development/plugin-development/server/middleware',
{ // '/development/plugin-development/server/acl',
title: 'Community', // '/development/plugin-development/server/events',
'title.zh-CN': '社区', // '/development/plugin-development/server/i18n',
type: 'group', // '/development/plugin-development/server/cli',
children: [ // '/development/plugin-development/server/app-manager',
'/contributing', // '/development/plugin-development/server/plugin-manager',
'/translations', // ],
'/roadmap', // },
'/faq', // {
'/thanks', // title: 'Client',
], // 'title.zh-CN': 'Client',
} // type: 'subMenu',
]; // children: [
// '/development/plugin-development/client/overview',
// {
// title: 'Providers',
// 'title.zh-CN': 'Providers',
// type: 'subMenu',
// children: [
// '/development/plugin-development/client/providers/acl',
// '/development/plugin-development/client/providers/antd',
// '/development/plugin-development/client/providers/api-client',
// '/development/plugin-development/client/providers/collection-manager',
// '/development/plugin-development/client/providers/i18n',
// '/development/plugin-development/client/providers/route-switch',
// '/development/plugin-development/client/providers/schema-component',
// '/development/plugin-development/client/providers/schema-initializer',
// ],
// },
// ],
// },
// ],
// },
// ],
// },
'/api': [
'/api/index',
'/api/env',
{
title: '@nocobase/server',
type: 'subMenu',
children: ['/api/server/application', '/api/server/plugin-manager', '/api/server/i18n'],
},
{
title: '@nocobase/database',
type: 'subMenu',
children: [
'/api/database/index',
'/api/database/collection',
'/api/database/field',
'/api/database/repository',
'/api/database/relation-repository',
'/api/database/operators',
],
},
{
title: '@nocobase/resourcer',
type: 'subMenu',
children: [
'/api/resourcer/index',
'/api/resourcer/resource',
'/api/resourcer/action',
'/api/resourcer/middleware',
],
},
{
title: '@nocobase/acl',
path: '/api/acl',
},
{
title: '@nocobase/client',
type: 'subMenu',
children: [
'/api/client/index',
'/api/client/application',
'/api/client/route-switch',
{
title: 'SchemaDesigner',
'title.zh-CN': 'SchemaDesigner',
type: 'subMenu',
children: [
'/api/client/schema-designer/schema-component',
'/api/client/schema-designer/schema-initializer',
'/api/client/schema-designer/schema-settings',
],
},
{
title: 'Extensions',
'title.zh-CN': 'Extensions',
type: 'subMenu',
children: [
'/api/client/extensions/schema-component',
'/api/client/extensions/collection-manager',
'/api/client/extensions/block-provider',
'/api/client/extensions/acl',
],
},
],
},
{
title: '@nocobase/cli',
path: '/api/cli',
},
{
title: '@nocobase/actions',
path: '/api/actions',
},
{
title: '@nocobase/sdk',
path: '/api/sdk',
},
],
};

View File

@ -0,0 +1,2 @@
# Actions API

1
docs/zh-CN/api/cli.md Normal file
View File

@ -0,0 +1 @@
# @nocobase/cli

View File

@ -0,0 +1,33 @@
# Application
```ts
const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`../plugins/${name}`);
},
});
```
## 属性
## 方法
## Providers
- APIClientProvider
- I18nextProvider
- AntdConfigProvider
- RemoteRouteSwitchProvider
- SystemSettingsProvider
- PluginManagerProvider
- SchemaComponentProvider
- SchemaInitializerProvider
- BlockSchemaComponentProvider
- AntdSchemaComponentProvider
- ACLProvider
- RemoteDocumentTitleProvider

View File

@ -0,0 +1 @@
# BlockProvider

View File

@ -0,0 +1,22 @@
# SchemaComponent
## Core
- SchemaComponentProvider
- SchemaComponent
- createDesignable
- useDesignable
- useCompile
## Common
- DndContext
- SortableItem
## And Design
- Action
- BlockItem
- Calendar
- CardItem
- Cascader

View File

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

View File

@ -0,0 +1,35 @@
# RouteSwitch
```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,22 @@
# SchemaComponent
## Core
- SchemaComponentProvider
- SchemaComponent
- createDesignable
- useDesignable
- useCompile
## Common
- DndContext
- SortableItem
## And Design
- Action
- BlockItem
- Calendar
- CardItem
- Cascader

View File

@ -0,0 +1 @@
# SchemaInitializer

View File

@ -0,0 +1 @@
# SchemaSettings

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', // 同一组关系指向同一张中间表
}
]
});
```

View File

@ -0,0 +1,753 @@
# Database
NocoBase 内置的数据库访问类,通过封装 [Sequelize](https://sequelize.org/) 提供了更加简单的数据库访问接口和统一化的 JSON 数据库表配置方式,同时也提供了扩展字段类型和查询操作符的能力。
Database 类继承自 EventEmitter可以通过 `db.on('event', callback)` 监听数据库事件,以及 `db.off('event', callback)` 移除监听。
## 包结构
可通过以下方式引入相关实体:
```ts
import Database, {
Field,
Collection,
Repository,
RelationRepository,
extend
} from '@nocobase/database';
```
## 构造函数
**签名**
* `constructor(options: DatabaseOptions)`
创建一个数据库实例。
**参数**
`options` 参数与 [Sequelize 的构造参数](https://sequelize.org/api/v6/class/src/sequelize.js~sequelize#instance-constructor-constructor)一致的部分会透传至 Sequelize同时 NocoBase 也会使用一些额外的参数:
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.host` | `string` | `'localhost'` | 数据库主机 |
| `options.port` | `number` | - | 数据库服务端口,根据使用的数据库有对应默认端口 |
| `options.username` | `string` | - | 数据库用户名 |
| `options.password` | `string` | - | 数据库密码 |
| `options.database` | `string` | - | 数据库名称 |
| `options.dialect` | `string` | `'mysql'` | 数据库类型 |
| `options.storage?` | `string` | `':memory:'` | SQLite 的存储模式 |
| `options.logging?` | `boolean` | `false` | 是否开启日志 |
| `options.define?` | `Object` | `{}` | 默认的表定义参数 |
| `options.tablePrefix?` | `string` | `''` | NocoBase 扩展,表名前缀 |
| `options.migrator?` | `UmzugOptions` | `{}` | NocoBase 扩展,迁移管理器相关参数,参考 [Umzug](https://github.com/sequelize/umzug/blob/main/src/types.ts#L15) 实现 |
**示例**
```ts
import Database from '@nocobase/database';
const app = new Database({
dialect: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'test',
tablePrefix: 'my_'
});
```
## 实例成员
### `sequelize`
初始化后的 Sequelize 实例,在需要使用 sequelize 底层方法时可以调用,相关信息可以直接参考 sequelize 的文档。
### `options`
初始化的配置参数,包含了 Sequelize 的配置参数和 NocoBase 的额外配置参数。
### `version`
连接的数据库的版本信息对象,可通过 `await db.version.satisfies(<sem_ver>)` 检查是否满足特定数据库版本要求。
**示例**
```ts
const r = await this.db.version.satisfies({
mysql: '>=8.0.17',
sqlite: '3.x',
postgres: '>=10',
});
```
## 迁移相关方法
### `addMigration()`
添加单个迁移文件。
**签名**
* `addMigration(options: MigrationItem)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.name` | `string` | - | 迁移文件名称 |
| `options.context?` | `string` | - | 迁移文件的 `up` 方法 |
| `options.migration?` | `typeof Migration` | - | 迁移文件的自定义类 |
| `options.up` | `Function` | - | 迁移文件的 `up` 方法 |
| `options.down` | `Function` | - | 迁移文件的 `down` 方法 |
**示例**
```ts
db.addMigration({
name: '20220916120411-test-1',
async up() {
const queryInterface = this.context.db.sequelize.getQueryInterface();
await queryInterface.query(/* your migration sqls */);
}
});
```
### `addMigrations()`
添加指定目录下的迁移文件。
**签名**
* `addMigrations(options: AddMigrationsOptions): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.directory` | `string` | `''` | 迁移文件所在目录 |
| `options.extensions` | `string[]` | `['js', 'ts']` | 文件扩展名 |
| `options.namespace?` | `string` | `''` | 命名空间 |
| `options.context?` | `Object` | `{ db }` | 迁移文件的上下文 |
**示例**
```ts
db.addMigrations({
directory: path.resolve(__dirname, './migrations'),
namespace: 'test'
});
```
## 工具方法
### `inDialect()`
判断当前数据库类型是否为指定类型。
**签名**
* `inDialect(dialect: string[]): boolean`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `dialect` | `string[]` | - | 数据库类型,可选值为 `mysql`/`postgres`/`sqlite` |
### `getTablePrefix()`
获取配置中的表名前缀。
**签名**
* `getTablePrefix(): string`
## 数据表配置
### `collection()`
定义一个数据表。该调用类似与 Sequelize 的 `define` 方法,只在内存中创建表结构,如需持久化到数据库,需要调用 `sync` 方法。
**签名**
* `collection(options: CollectionOptions): Collection`
**参数**
`options` 所有配置参数与 `Collection` 类的构造函数一致,参考 [Collection](/api/server/database/collection#构造函数)。
**事件**
* `'beforeDefineCollection'`:在定义表之前触发。
* `'afterDefineCollection'`:在定义表之后触发。
**示例**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'float',
name: 'price',
}
]
});
// sync collection as table to db
await db.sync();
```
### `getCollection()`
获取已定义的数据表。
**签名**
* `getCollection(name: string): Collection`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 表名 |
**示例**
```ts
const collection = db.getCollection('books');
```
### `hasCollection()`
判断是否已定义指定的数据表。
**签名**
* `hasCollection(name: string): boolean`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 表名 |
**示例**
```ts
db.collection({ name: 'books' });
db.hasCollection('books'); // true
db.hasCollection('authors'); // false
```
### `removeCollection()`
移除已定义的数据表。仅在内存中移除,如需持久化,需要调用 `sync` 方法。
**签名**
* `removeCollection(name: string): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 表名 |
**事件**
* `'beforeRemoveCollection'`:在移除表之前触发。
* `'afterRemoveCollection'`:在移除表之后触发。
**示例**
```ts
db.collection({ name: 'books' });
db.removeCollection('books');
```
### `import()`
导入文件目录下所有文件作为 collection 配置载入内存。
**签名**
* `async import(options: { directory: string; extensions?: ImportFileExtension[] }): Promise<Map<string, Collection>>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.directory` | `string` | - | 要导入的目录路径 |
| `options.extensions` | `string[]` | `['ts', 'js']` | 扫描特定后缀 |
**示例**
`./collections/books.ts` 文件定义的 collection 如下:
```ts
export default {
name: 'books',
fields: [
{
type: 'string',
name: 'title',
}
]
};
```
在插件加载时导入相关配置:
```ts
class Plugin {
async load() {
await this.app.db.import({
directory: path.resolve(__dirname, './collections'),
});
}
}
```
## 扩展注册与获取
### `registerFieldTypes()`
注册自定义字段类型。
**签名**
* `registerFieldTypes(fieldTypes: MapOf<typeof Field>): void`
**参数**
`fieldTypes` 是一个键值对,键为字段类型名称,值为字段类型类。
**示例**
```ts
import { Field } from '@nocobase/database';
class MyField extends Field {
// ...
}
db.registerFieldTypes({
myField: MyField,
});
```
### `registerModels()`
注册自定义数据模型类。
**签名**
* `registerModels(models: MapOf<ModelCtor<any>>): void`
**参数**
`models` 是一个键值对,键为数据模型名称,值为数据模型类。
**示例**
```ts
import { Model } from '@nocobase/database';
class MyModel extends Model {
// ...
}
db.registerModels({
myModel: MyModel,
});
db.collection({
name: 'myCollection',
model: 'myModel'
});
```
### `registerRepositories()`
注册自定义数据仓库类。
**签名**
* `registerRepositories(repositories: MapOf<RepositoryType>): void`
**参数**
`repositories` 是一个键值对,键为数据仓库名称,值为数据仓库类。
**示例**
```ts
import { Repository } from '@nocobase/database';
class MyRepository extends Repository {
// ...
}
db.registerRepositories({
myRepository: MyRepository,
});
db.collection({
name: 'myCollection',
repository: 'myRepository'
});
```
### `registerOperators()`
注册自定义数据查询操作符。
**签名**
* `registerOperators(operators: MapOf<OperatorFunc>)`
**参数**
`operators` 是一个键值对,键为操作符名称,值为操作符比较语句生成函数。
**示例**
```ts
db.registerOperators({
$dateOn(value) {
return {
[Op.and]: [{ [Op.gte]: stringToDate(value) }, { [Op.lt]: getNextDay(value) }],
};
}
});
db.getRepository('books').count({
filter: {
createdAt: {
// registered operator
$dateOn: '2020-01-01',
}
}
});
```
### `getModel()`
获取已定义的数据模型类。如果没有在之前注册自定义模型类,将返回 Sequelize 默认的模型类。默认名称与 collection 定义的名称相同。
**签名**
* `getModel(name: string): Model`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 已注册的模型名 |
**示例**
```ts
db.registerModels({
books: class MyModel extends Model {}
});
const ModelClass = db.getModel('books');
console.log(ModelClass.prototype instanceof MyModel) // true
```
注:从 collection 中获取的模型类并不与注册时的模型类严格相等,而是继承自注册时的模型类。由于 Sequelize 的模型类在初始化过程中属性会被修改,所以 NocoBase 自动处理了这个继承关系。除类不相等以外,其他所有定义都可以正常使用。
### `getRepository()`
获取自定义的数据仓库类。如果没有在之前注册自定义数据仓库类,将返回 NocoBase 默认的数据仓库类。默认名称与 collection 定义的名称相同。
数据仓库类主要用于基于数据模型的增删改查等操作,参考 [数据仓库](/api/server/database/repository)。
**签名**
* `getRepository(name: string): Repository`
* `getRepository(name: string, relationId?: string | number): Repository`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `name` | `string` | - | 已注册的数据仓库名 |
| `relationId` | `string` \| `number` | - | 关系数据的外键值 |
当名称是形如 `'tables.relactions'` 的带关联的名称时,将返回关联的数据仓库类。如果提供了第二个参数,数据仓库在使用时(查询、修改等)会基于关系数据的外键值。
**示例**
假设有两张数据表_文章_与_作者_并且文章表中有一个外键指向作者表
```ts
const AuthorsRepo = db.getRepository('authors');
const author1 = AuthorsRepo.create({ name: 'author1' });
const PostsRepo = db.getRepository('authors.posts', author1.id);
const post1 = AuthorsRepo.create({ title: 'post1' });
asset(post1.authorId === author1.id); // true
```
## 数据库事件
### `on()`
监听数据库事件。
Database 除了对 sequelize 原生的事件封装以外,还提供以下可监听事件类型:
| 事件名称 | 描述 |
| --- | --- |
| `'beforeDefineCollection'` | 定义 collection 之前触发 |
| `'afterDefineCollection'` | 定义 collection 之后触发 |
| `'beforeRemoveCollection'` | 移除 collection 之前触发 |
| `'afterRemoveCollection'` | 移除 collection 之后触发 |
| `<sequelize_model_global_event>` | 所有 sequelize 的全局事件均可通过此方式监听,详见示例部分 |
| `<model_name>.<sequelize_model_event>` | 所有 sequelize model 的事件均可通过此方式监听,详见示例部分 |
| `<model_name>.afterCreateWithAssociations` | NocoBase 扩展的当连同关联数据一并创建记录成功后触发的事件(使用 Repository 的 create 方法会触发) |
| `<model_name>.afterUpdateWithAssociations` | NocoBase 扩展的当连同关联数据一并更新记录成功后触发的事件(使用 Repository 的 update 方法会触发) |
| `<model_name>.afterSaveWithAssociations` | NocoBase 扩展的当连同关联数据一并创建或更新记录成功后触发的事件(使用 Repository 的 create/update 方法都会触发) |
**签名**
* `on(event: string, listener: (...args: any[]) => void | Promise<void>): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| event | string | - | 事件名称 |
| listener | Function | - | 事件监听器 |
**示例**
监听 books 表事件,当创建新记录成功后触发:
```ts
db.on('books.afterCreate', async (model, options) => {
const { transaction } = options;
const result = await model.constructor.findByPk(model.id, {
transaction
});
console.log(result);
});
```
监听 books 表,当连同关联数据一并创建成功后触发的事件:
```ts
db.on('books.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options;
const result = await model.constructor.findByPk(model.id, {
transaction
});
console.log(result);
});
```
监听任意表的删除事件:
```ts
db.on('afterDestroy', async (model, options) => {
console.log(model);
});
```
### `off()`
移除事件监听函数。
**签名**
* `off(name: string, listener: Function)`
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| name | string | - | 事件名称 |
| listener | Function | - | 事件监听器 |
**示例**
```ts
const listener = async (model, options) => {
console.log(model);
};
db.on('afterCreate', listener);
db.off('afterCreate', listener);
```
## 数据库操作
### `auth()`
数据库连接验证。可以用于确保应用与数据已建立连接。
**签名**
* `auth(options: QueryOptions & { retry?: number } = {}): Promise<boolean>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options?` | `Object` | - | 验证选项 |
| `options.retry?` | `number` | `10` | 验证失败时重试次数 |
| `options.transaction?` | `Transaction` | - | 事务对象 |
| `options.logging?` | `boolean \| Function` | `false` | 是否打印日志 |
**示例**
```ts
await db.auth();
```
### `reconnect()`
重新连接数据库。
**示例**
```ts
await db.reconnect();
```
### `closed()`
判断数据库是否已关闭连接。
**签名**
* `closed(): boolean`
### `close()`
关闭数据库连接。等同于 `sequelize.close()`
### `sync()`
同步数据库表结构。等同于 `sequelize.sync()`,参数参考 [Sequelize 文档](https://sequelize.org/api/v6/class/src/sequelize.js~sequelize#instance-method-sync)。
### `clean()`
清空数据库,将删除所有数据表。
**签名**
* `clean(options: CleanOptions): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.drop` | `boolean` | `false` | 是否移除所有数据表 |
| `options.skip` | `string[]` | - | 跳过的表名配置 |
| `options.transaction` | `Transaction` | - | 事务对象 |
**示例**
移除除 `users` 表以外的所有表。
```ts
await db.clean({
drop: true,
skip: ['users']
})
```
## 包级导出
### `defineCollection()`
创建一个数据表的配置内容。
**签名**
* `defineCollection(name: string, config: CollectionOptions): CollectionOptions`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `collectionOptions` | `CollectionOptions` | - | 与所有 `db.collection()` 的参数相同 |
**示例**
对于要被 `db.import()` 导入的数据表配置文件:
```ts
import { defineCollection } from '@nocobase/database';
export default defineCollection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
},
],
});
```
### `extendCollection()`
扩展已在内存中的表结构配置内容,主要用于 `import()` 方法导入的文件内容。该方法是 `@nocobase/database` 包导出的顶级方法,不通过 db 实例调用。也可以使用 `extend` 别名。
**签名**
* `extendCollection(collectionOptions: CollectionOptions, mergeOptions?: MergeOptions): ExtendedCollectionOptions`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `collectionOptions` | `CollectionOptions` | - | 与所有 `db.collection()` 的参数相同 |
| `mergeOptions?` | `MergeOptions` | - | npm 包 [deepmerge](https://npmjs.com/package/deepmerge) 的参数 |
**示例**
原始 books 表定义books.ts
```ts
export default {
name: 'books',
fields: [
{ name: 'title', type: 'string' }
]
}
```
扩展 books 表定义books.extend.ts
```ts
import { extend } from '@nocobase/database';
// 再次扩展
export default extend({
name: 'books',
fields: [
{ name: 'price', type: 'number' }
]
});
```
以上两个文件如在调用 `import()` 时导入,通过 `extend()` 再次扩展以后books 表将拥有 `title``price` 两个字段。
此方法在扩展已有插件已定义的表结构时非常有用。

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,87 @@
# RelationRepository
关系型数据仓库抽象类。
## 基类属性
### `db`
### `sourceCollection`
### `targetCollection`
### `associationField`
### `sourceKeyValue`
### `sourceInstance`
## HasOneRepository
### `find()`
### `update()`
### `destroy()`
### `remove()`
### `set()`
## BelongsToRepository
### `find()`
### `update()`
### `destroy()`
### `remove()`
### `set()`
## HasManyRepository
### `count()`
### `find()`
### `findOne()`
### `findAndCount()`
### `create()`
### `update()`
### `destroy()`
### `add()`
### `remove()`
### `set()`
## BelongsToManyRepository
### `count()`
### `find()`
### `findOne()`
### `findAndCount()`
### `create()`
### `update()`
### `destroy()`
### `add()`
### `remove()`
### `set()`
### `toggle()`

View File

@ -0,0 +1,355 @@
# 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`
对应的数据模型类。
## 实例方法
### `count()`
从数据库查询特定条件的数据总数。相当于 Sequelize 中的 `Model.count()`
**签名**
* `count(options?: CountOptions): Promise<number>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.filter?` | `Filter` | `{}` | 查询条件 |
| `options.fields?` | `string[]` | - | 计数列 |
| `options.transaction?` | `Transaction` | - | 事务 |
`filter` 为兼容 Sequelize 查询条件的扩展形式,更多操作符支持详见 [Operators](./operators.md)。
**示例**
```ts
const books = db.getRepository('books');
const count = await books.count({
filter: {
title: '三字经'
}
});
```
### `find()`
从数据库查询特定条件的数据集。相当于 Sequelize 中的 `Model.findAll()`
**签名**
* `async find(options?: FindOptions): Promise<Model[]>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.filterByTk?` | `number \| string` | `{}` | 主键值 |
| `options.filter?` | `Filter` | `{}` | 查询条件 |
| `options.fields?` | `string[]` | - | 查询列 |
| `options.appends?` | `string[]` | - | 追加关系数据列 |
| `options.except?` | `string[]` | - | 排除数据列 |
| `options.sort?` | `string[] \| string` | - | 排序 |
| `options.limit?` | `number` | - | 限制条数 |
| `options.offset?` | `number` | - | 跳过条数 |
| `options.transaction?` | `Transaction` | - | 事务 |
`fields``except` 通常只使用其一,相当于 Sequelize 中 `attributes``include` / `exclude`。`appends` 通常用于关系字段。
**示例**
```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>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.filterByTk?` | `number \| string` | `{}` | 主键值 |
| `options.filter?` | `Filter` | `{}` | 查询条件 |
| `options.fields?` | `string[]` | - | 查询列 |
| `options.appends?` | `string[]` | - | 追加关系数据列 |
| `options.except?` | `string[]` | - | 排除数据列 |
| `options.sort?` | `string[] \| string` | - | 排序 |
| `options.offset?` | `number` | - | 跳过条数 |
| `options.transaction?` | `Transaction` | - | 事务 |
大部分参数与 `find()` 相同,不同之处在于 `findOne()` 只返回单条数据,所以不需要 `limit` 参数,且查询时始终为 `1`
**示例**
```ts
const posts = db.getRepository('posts');
const result = await posts.findOne({
filterByTk: 1,
});
```
### `findAndCount()`
从数据库查询特定条件的数据集和总数。相当于 Sequelize 中的 `Model.findAndCountAll()`
**签名**
* `async findAndCount(options?: FindAndCountOptions): Promise<[Model[], number]>`
**参数**
所有参数与 [`find()`](#find) 相同。
### `create()`
向数据表插入一条新创建的数据。相当于 Sequelize 中的 `Model.create()`。当要创建的数据对象携带关系字段的信息时,会一并创建或更新相应的关系数据记录。
**签名**
* `async create<M extends Model>(options: CreateOptions): Promise<M>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.values` | `M` | `{}` | 插入的数据对象 |
| `options.whitelist?` | `string[]` | - | `values` 字段的白名单,只有名单内的字段会被存储 |
| `options.blacklist?` | `string[]` | - | `values` 字段的黑名单,名单内的字段不会被存储 |
| `options.transaction?` | `Transaction` | - | 事务 |
如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```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[]>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.records` | `Model[]` | - | 插入的数据对象数组 |
| `options.whitelist?` | `string[]` | - | `values` 字段的白名单,只有名单内的字段会被存储 |
| `options.blacklist?` | `string[]` | - | `values` 字段的黑名单,名单内的字段不会被存储 |
| `options.transaction?` | `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>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.filterByTk` | `number \| string` | - | 查询主键 |
| `options.filter` | `Filter` | - | 查询条件 |
| `options.values` | `M` | `{}` | 更新的数据对象 |
| `options.whitelist?` | `string[]` | - | `values` 字段的白名单,只有名单内的字段会被存储 |
| `options.blacklist?` | `string[]` | - | `values` 字段的黑名单,名单内的字段不会被存储 |
| `options.transaction?` | `Transaction` | - | 事务 |
`filterByTk``filter` 至少要传其一。如果没有传入事务参数,该方法会自动创建一个内部事务。
**示例**
```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>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.filterByTk` | `number \| string` | - | 查询主键 |
| `options.filter` | `Filter` | - | 查询条件 |
| `options.transaction?` | `Transaction` | - | 事务 |
`filterByTk``filter` 至少要传其一。如果没有传入事务参数,该方法会自动创建一个内部事务。
## 关系数据仓库类
通过 `db.getRepository('<name.associatedField>', <id>)` 方法获取到的关系数据仓库管理实例,在调用方法时,会自动在查询条件中加入当前表的主键值。除此以外,还拥有一些基于不同关系类型特有的成员和关系操作方法。
### `set()`
相当于 Sequelize 的 `record.setXxx()` 方法,用于设置关联数据。对一的关系参数为对应的外键,对多的关系参数为对应的外键数组。如果关系类型为 `BelongsToMany`,则会自动创建中间表数据。
**签名**
* `async set(association: string | number | string[] | number[]): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `association` | `string \| number \| string[] \| number[]` | - | 关联数据的外键,对多时可用数组 |
### `add()`
相当于 Sequelize 的 `record.addXxx()` 方法,用于附加关联,仅针对对多关系。
**签名**
* `async add(association: string | number | string[] | number[]): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `association` | `string \| number \| string[] \| number[]` | - | 关联数据的外键 |
### `remove()`
相当于 Sequelize 的 `record.removeXxx()` 方法,用于移除关联,仅针对对多关系。
**签名**
* `async remove(association: string | number | string[] | number[]): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `association` | `string \| number \| string[] \| number[]` | - | 关联数据的外键 |

11
docs/zh-CN/api/index.md Normal file
View File

@ -0,0 +1,11 @@
# 概览
## 内置模块列表
| 模块 | 包名 | 描述 |
| --- | --- | --- |
| [Server](./server) | `@nocobase/server` | 服务端应用 |
| [Database](./database) | `@nocobase/database` | 数据库访问层 |
| [Resourcer](./resourcer) | `@nocobase/resourcer` | 资源与路由映射 |
| [ACL](./acl) | `@nocobase/acl` | 访问控制表 |
| [Client](./client) | `@nocobase/client` | 客户端应用 |

View File

@ -0,0 +1,103 @@
# 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 得到的是经过多次合并的最终参数。
参数的合并过程提供了针对操作处理的可扩展性,可以通过自定义中间件的方式按业务需求进行参数的前置解析和处理,例如表单提交的参数验证就可以在此环节实现。
预设的参数与默认合并策略如下表:
| 参数名 | 类型 | 默认值 | 合并策略 | 描述 |
| --- | --- | --- | --- | --- |
| `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` | - | shallow merge | 操作提交的数据 |
参数中还包含请求资源路由的描述部分,具体如下:
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `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.mergeParam({
values: {
id: Math.random().toString(36).substr(2, 10),
publishedAt: new Date(),
}
});
await next();
}
]
});
```

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,4 +1,4 @@
# JavaScript SDK # @nocobase/sdk
## APIClient ## APIClient

View File

@ -0,0 +1,267 @@
# Application
NocoBase 的应用程序类,服务端的应用进程将实例化该类并启动。因为继承自 [Koa](https://koajs.com/),所以也可以直接调用 Koa 实例的方法。
同时因为 Koa 还继承自 [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter),所以也可以通过 `app.on` 监听事件。
## 构造函数
**签名**
* `constructor(options: ApplicationOptions)`
创建一个应用实例。
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.database` | `IDatabaseOptions` or `Database` | `{}` | 数据库配置 |
| `options.resourcer` | `ResourcerOptions` | `{}` | 资源路由配置 |
| `options.cors` | `CorsOptions` | `{}` | 跨域配置,参考 [@koa/cors](https://npmjs.com/package/@koa/cors) |
| `options.dataWrapping` | `boolean` | `true` | 是否包装响应数据,`true` 则将把通常的 `ctx.body` 包装为 `{ data, meta }` 的结构。 |
| `options.registerActions` | `boolean` | `true` | 是否注册默认的 action |
| `options.i18n` | `I18nOptions` | `{}` | 国际化配置,参考 [i18next](https://www.npmjs.com/package/i18next) |
| `options.plugins` | `PluginConfiguration[]` | `[]` | 默认启用的插件配置 |
**示例**
```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: '*',
}
});
```
## 实例成员
### `db`
应用初始化自动创建的数据库实例,通过 `@nocobase/database` 封装的数据库访问类,相关方法参考 [Database](./database)。
### `resourcer`
应用初始化自动创建的资源路由管理实例,相关方法参考 [Resourcer](./resourcer)。
### `cli`
应用初始化自动创建的命令行工具实例,相关方法参考 [CLI](../cli)。
### `acl`
应用初始化自动创建的访问控制表实例,相关方法参考 [ACL](./acl)。
### `i18n`
应用初始化自动创建的国际化工具实例,相关方法参考 [I18n](./i18n)。
### `appManager`
应用初始化自动创建的应用管理实例,相关方法参考 [AppManager](./app-manager)。
### `pm`
应用初始化自动创建的插件管理器实例,相关方法参考 [PluginManager](./plugin-manager)。
### `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.getBearerToken()` | `Function` | 获取请求头中的 bearer token |
## 实例方法
### `getVersion()`
获取当前版本。
**签名**
* `getVersion(): string`
**示例**
```ts
console.log(app.getVersion()); // 0.7.4-alpha.7
```
### `plugin()`
向应用中注册插件的快捷方法,等同于 `app.pm.add()`,参考 [PluginManager](./plugin-manager#add)。
### `loadPluginConfig()`
批量注册插件与默认参数。
**签名**
* `loadPluginConfig(pluginsConfigurations: PluginConfiguration[]): void`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `pluginsConfigurations[]` | `string` | - | 插件的 npm 包名 |
| `pluginsConfigurations[]` | `[string, any]` | - | 插件的 npm 包名与默认参数 |
### `getPlugin()`
获取已注册的插件实例,等同于 `app.pm.get(name)`,参考 [PluginManager](./plugin-manager#get)。
### `use()`
注册应用级中间件的快捷方法,等同于 `koa.use()`
### `on()`
订阅应用级事件,主要与生命周期相关,等同于 `eventEmitter.on()`。所有可订阅事件参考 [事件](#事件)。
### `collection()`
向数据库注册数据表模型的快捷方法,等同于 `app.db.collection()`,参考 [Database](./database#collection)。
### `resource()`
向资源路由管理器注册资源的快捷方法,等同于 `app.resourcer.define()`,参考 [Resourcer](./resourcer#define)。
### `actions()`
向资源路由管理器注册 action 的快捷方法,等同于 `app.resourcer.registerActions()`,参考 [Resourcer](./resourcer#registerActions)。
### `command()`
向命令行工具注册命令的快捷方法,等同于 `app.cli.command()`,参考 [cli](./cli)。
### `load()`
应用生命周期方法,用于加载所有已注册的插件,等同于 `app.pm.load()`,参考 [PluginManager](./plugin-manager#load)。
**签名**
* `async load(): Promise<void>`
### `start()`
启动应用,如果配置了监听的端口,将启动监听,之后应用即可接受 HTTP 请求。
**签名**
* `async start(options: StartOptions): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.listen?` | `ListenOptions` | `{}` | HTTP 监听参数对象 |
| `options.listen.port?` | `number` | 13000 | 端口 |
| `options.listen.host?` | `string` | `'localhost'` | 域名 |
**事件**
`start()` 调用会触发两个事件:
* `'beforeStart'`:在应用启动前触发。
* `'afterStart'`:在应用启动后触发。
### `stop()`
停止应用,如果应用正在监听,将停止监听,并关闭数据库连接。
**签名**
* `async stop(options: any): Promise<void>`
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options?` | `any` | - | 将透传至停止事件的参数 |
**事件**
`stop()` 调用会触发两个事件:
* `'beforeStop'`:在应用停止前触发。
* `'afterStop'`:在应用停止后触发。
### `destroy()`
销毁并停止应用,包含 `stop()` 的调用。
**事件**
`destroy()` 调用会触发两个事件:
* `'beforeDestroy'`:在应用销毁前触发。
* `'afterDestroy'`:在应用销毁后触发。
插件相关的销毁事件可以基于该事件进行扩展。
### `install()`
应用初始化安装调用接口,用于安装应用的基础数据,包括:
* 同步已通过 `db.collection()``db.import()` 注入的数据库表结构到数据库。
* 调用所有已注册插件实例的 `install()` 方法,执行相应安装过程。
**事件**
* `'beforeInstall'`:在应用安装前触发。
* `'afterInstall'`:在应用安装后触发。
### `upgrade()`
应用调用接口,当更新新版本代码后通过命令行工具调用,用于执行应用的升级过程,包括:
* 数据库迁移工具执行。
**事件**
* `'beforeUpgrade'`:在应用升级前触发。
* `'afterUpgrade'`:在应用升级后触发。
## 事件
应用生命周期会触发相应事件,扩展开发可以基于这些事件进行。
| 事件名称 | 是否异步 | 描述 |
| --- | --- | --- |
| `'beforeInstall'` | 是 | 安装前触发 |
| `'afterInstall'` | 是 | 安装后触发 |
| `'beforeUpgrade'` | 是 | 升级前触发 |
| `'afterUpgrade'` | 是 | 升级后触发 |
| `'beforeStart'` | 是 | 启动前触发 |
| `'afterStart'` | 是 | 启动后触发 |
| `'beforeStop'` | 是 | 停止前触发 |
| `'afterStop'` | 是 | 停止后触发 |
| `'beforeDestroy'` | 是 | 销毁前触发 |
| `'afterDestroy'` | 是 | 销毁后触发 |
**示例**
```ts
app.on('beforeStart', async () => {
console.log('app before start');
});
```

View File

@ -0,0 +1,33 @@
# PluginManager
应用插件管理器的实例,由应用自动创建,可以通过 `app.pm` 访问。
## 实例方法
### `add()`
**签名**
* `add(pluginClass: PluginConstructor, options?: any): Plugin`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `pluginClass` | `PluginConstructor` | - | 插件类,参考 [Plugin 类](./plugin) |
**返回值**
对应插件类的实例
**示例**
```ts
import Application, { Plugin } from '@nocobase/server';
class MyPlugin extends Plugin {}
const app = new Application();
app.pm.add(MyPlugin, { a: 1 });
```

View File

@ -0,0 +1 @@
# Collections & Fields

View File

@ -0,0 +1 @@
# Commands

View File

@ -0,0 +1 @@
# Hooks

View File

@ -0,0 +1 @@
# Overview

View File

@ -0,0 +1 @@
# Middleware

View File

@ -0,0 +1 @@
# Migration

View File

@ -0,0 +1 @@
# Resources & Actions

View File

@ -0,0 +1 @@
# UI Router

View File

@ -0,0 +1,2 @@
# ACL

View File

@ -0,0 +1 @@
# Collection Manager

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

View File

@ -0,0 +1,515 @@
# 扩展 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 类型,例子见 [void 和 object 类型 schema 的嵌套](#void-和-object-类型-schema-的嵌套)
- `<RecursionField />` 嵌套,适用于各种场景,例子见 [array 类型 schema 的嵌套](#array-类型-schema-的嵌套)
注意:仅 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>
);
};
```
各类型对比array 和 string 并不会渲染 World 节点
```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',
},
void: {
type: 'void',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'void',
'x-component': 'World',
},
},
},
title2: {
type: 'void',
'x-content': 'Object schema',
},
object: {
type: 'object',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title3: {
type: 'void',
'x-content': 'Array schema',
},
array: {
type: 'array',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title4: {
type: 'void',
'x-content': 'String schema',
},
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;
// 组件的子节点,简单使用
['x-content']?: any;
// 展示状态,默认为 'visible'
['x-display']?: 'none' | 'hidden' | 'visible';
// children 节点 schema
properties?: Record<string, ISchema>;
// 以下仅字段组件时使用
// 字段联动
['x-reactions']?: SchemaReactions;
// 字段 UI 交互模式,默认为 'editable'
['x-pattern']?: 'editable' | 'disabled' | 'readOnly' | 'readPretty';
// 字段校验
['x-validator']?: FieldValidator;
// 默认数据
default: ?:any;
// 设计器相关
// 设计器组件(工具栏)
['x-designer']?: any;
// 初始化器组件(工具栏)
['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,42 @@
# 概述
## 基本概念
NocoBase 的核心是一个基于微内核、插件化设计的开发框架。在此基础上天热的支持任意的扩展开发。
## 特性
## 扩展能力
基于 NocoBase 的微内核设计,在很多模块都考虑相应合理的可扩展性,并向外部暴露了一些扩展点。例如:
* 数据库字段类型
* 已有数据库表的字段
* 通用操作和对特定的资源的操作
* 针对资源和操作的预处理和后处理
* 字段的前端展示组件
* 页面的区块类型
* 用户插件的登入认证方法
* 工作流插件的节点类型和触发类型
* ……
在具体业务中可以根据需求基于以上扩展点进行相应的扩展。
## 插件生命周期
### 下载
### 注册
### 安装
### 启用
### 加载
### 停用
### 卸载
## 学习路线

View File

@ -1 +0,0 @@
# 快速入门

View File

@ -1 +0,0 @@
# PluginManager

View File

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

View File

@ -0,0 +1 @@
# Building

View File

@ -0,0 +1 @@
# Testing

View File

@ -15,7 +15,7 @@ https://github.com/nocobase/nocobase/tree/main/packages/core/client/src/locale
请复制 en_US.ts命名为想要新增的语言的名字然后对其中的字符串进行翻译。翻译完成之后请通过 pull request 提交给 NocoBase我们将会把它加入语言列表。之后你将在系统配置中看到新增的语言在这里你可以配置需要显示哪些语言供用户选择。 请复制 en_US.ts命名为想要新增的语言的名字然后对其中的字符串进行翻译。翻译完成之后请通过 pull request 提交给 NocoBase我们将会把它加入语言列表。之后你将在系统配置中看到新增的语言在这里你可以配置需要显示哪些语言供用户选择。
<img src="./images/enabled-languages.jpg" style="max-width: 800px;"/> <img src="../images/enabled-languages.jpg" style="max-width: 800px;"/>
下面的表格中列出了 Language Culture Name, Locale File Name, Display Name. 下面的表格中列出了 Language Culture Name, Locale File Name, Display Name.

View File

@ -0,0 +1,96 @@
# 编写第一个插件
在此之前,需要先安装好 NocoBase
- create-nocobase-app 安装
- Git 源码安装
相关安装、启动命令如下:
```bash
cd my-nocobase-app
yarn install
yarn nocobase install
yarn dev
```
插件开发和调试,需要在开发环境并通过 `yarn dev` 启动。做好准备之后,我们就可以开始 NocoBase 的插件开发之旅了。
## 创建插件
首先,你可以通过 CLI 快速的创建一个初始化的插件,命令如下:
```bash
yarn pm create hello
```
新建的插件,会放置在 `packages/plugins/hello` 目录下。
## 插件目录结构
```ts
|- /hello
|- /src
|- /client
|- /server
|- client.d.ts
|- client.js
|- package.json
|- server.d.ts
|- server.js
```
## 编写插件
插件的主体文件在 `packages/plugins/hello/src/server/plugin.ts`,修改为:
```ts
import { InstallOptions, Plugin } from '@nocobase/server';
export class Hello extends Plugin {
initialize() {
// TODO
}
beforeLoad() {
// TODO
}
async load() {
// TODO
// Visit: http://localhost:13000/api/hello:get
this.app.resource({
name: 'hello',
actions: {
async get(ctx, next) {
ctx.body = `Hello plugin1!`;
next();
},
},
});
this.app.acl.allow('hello', 'get');
}
async install(options: InstallOptions) {
// TODO
}
}
export default Hello;
```
## 注册插件
```bash
yarn pm add hello
```
## 激活插件
```bash
yarn pm enable hello
```
## 体验插件功能
访问地址 http://localhost:13000/api/hello:get

View File

@ -1,2 +0,0 @@
# 常见问题

View File

@ -0,0 +1,2 @@
# 部署

View File

@ -1,4 +1,11 @@
# Docker 安装 (👍 推荐) ---
group:
path: /getting-started/installation/docker-compose
title: Docker 安装 (👍 推荐)
order: 1
---
# Docker 安装
## 0. 先决条件 ## 0. 先决条件

View File

@ -4,9 +4,9 @@
NocoBase 支持三种安装方式: NocoBase 支持三种安装方式:
- [Docker 安装(推荐)](./docker-compose.md) - [Docker 安装(推荐)](./installation/docker-compose.md)
- [create-nocobase-app 安装](./create-nocobase-app.md) - [create-nocobase-app 安装](./installation/create-nocobase-app.md)
- [Git 源码安装](./git-clone.md) - [Git 源码安装](./installation/git-clone.md)
## 如何选择 ## 如何选择

View File

@ -1,4 +1,4 @@
# 与众不同之处 # 功能特色
## 1. “数据结构”与“使用界面”分离 ## 1. “数据结构”与“使用界面”分离
@ -6,12 +6,12 @@
NocoBase 采用数据结构与使用界面分离的设计思路,可以为数据表创建任意数量、任意形态的区块(数据视图),每个区块里可以定义不同的样式、文案、操作。这样既兼顾了无代码的简单操作,又具备了原生开发的灵活性。 NocoBase 采用数据结构与使用界面分离的设计思路,可以为数据表创建任意数量、任意形态的区块(数据视图),每个区块里可以定义不同的样式、文案、操作。这样既兼顾了无代码的简单操作,又具备了原生开发的灵活性。
![2.collection-block.png](./user-manual/introduction/important-features/2.collection-block.png) ![2.collection-block.png](./features/2.collection-block.png)
## 2. “配置”与“使用”融为一体 ## 2. “配置”与“使用”融为一体
NocoBase 可以开发复杂和有特色的业务系统,但这并意味着需要复杂和专业的操作。只需一次点击,就可以在使用界面上显示出配置选项,这意味着具备系统配置权限的管理员可以用所见即所得的操作方式,直接配置用户的使用界面。 NocoBase 可以开发复杂和有特色的业务系统,但这并意味着需要复杂和专业的操作。只需一次点击,就可以在使用界面上显示出配置选项,这意味着具备系统配置权限的管理员可以用所见即所得的操作方式,直接配置用户的使用界面。
![2.user-root.gif](./user-manual/introduction/important-features/2.user-root.gif) ![2.user-root.gif](./features/2.user-root.gif)
## 3. 功能即插件 ## 3. 功能即插件

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.8 MiB

View File

@ -1,4 +1,4 @@
# 介绍 # 概述
![](https://www.nocobase.com/images/demo/11.png) ![](https://www.nocobase.com/images/demo/11.png)

View File

@ -31,27 +31,27 @@
接下来,点击“数据表配置”按钮,进入数据表配置界面,创建第一个 Collection `Customers` 接下来,点击“数据表配置”按钮,进入数据表配置界面,创建第一个 Collection `Customers`
![1.customers.gif](./5-minutes-to-get-started/1.customers.gif) ![1.customers.gif](./quickstart/1.customers.gif)
然后点击“字段配置”,为`Customers` 添加 name 字段,它是单行文本类型。 然后点击“字段配置”,为`Customers` 添加 name 字段,它是单行文本类型。
![2.field.gif](./5-minutes-to-get-started/2.field.gif) ![2.field.gif](./quickstart/2.field.gif)
用同样的方法,为`Customers` 添加 Birthday、Gender、Phone它们分别是日期类型、单项选择类型、手机号码类型。 用同样的方法,为`Customers` 添加 Birthday、Gender、Phone它们分别是日期类型、单项选择类型、手机号码类型。
![1.fields.jpg](./5-minutes-to-get-started/1.fields.jpg) ![1.fields.jpg](./quickstart/1.fields.jpg)
用同样的方法,创建 Collection `Products`、`Orders`、`Order List` 以及它们的字段。 用同样的方法,创建 Collection `Products`、`Orders`、`Order List` 以及它们的字段。
![1.collections.jpg](./5-minutes-to-get-started/1.collections.jpg) ![1.collections.jpg](./quickstart/1.collections.jpg)
其中,对于关系字段,我们要选择正确的类型,从而建立数据表之间的关联。我们以`Orders`为例,创建 Customer 字段,选择 **多对一** 关系,关联到`Customers`。 其中,对于关系字段,我们要选择正确的类型,从而建立数据表之间的关联。我们以`Orders`为例,创建 Customer 字段,选择 **多对一** 关系,关联到`Customers`。
![1.relation.jpg](./5-minutes-to-get-started/1.relation.jpg) ![1.relation.jpg](./quickstart/1.relation.jpg)
创建关系字段后,我们可以在被关联的 Collection 里看到自动生成的反向关联字段。比如在`Customers`中看到自动生成的 Orders 字段,这样我们在`Customers`的区块里可以调用`Orders`的数据。 创建关系字段后,我们可以在被关联的 Collection 里看到自动生成的反向关联字段。比如在`Customers`中看到自动生成的 Orders 字段,这样我们在`Customers`的区块里可以调用`Orders`的数据。
![1.auto.relation.jpg](./5-minutes-to-get-started/1.auto.relation.jpg) ![1.auto.relation.jpg](./quickstart/1.auto.relation.jpg)
将数据表和字段创建完成后,我们开始制作界面。 将数据表和字段创建完成后,我们开始制作界面。
@ -61,11 +61,11 @@
点击界面配置按钮,进入界面配置模式。在界面配置模式下,我们可以添加菜单项,添加页面,在页面内布置区块。 点击界面配置按钮,进入界面配置模式。在界面配置模式下,我们可以添加菜单项,添加页面,在页面内布置区块。
![1.editor.gif](./5-minutes-to-get-started/1.editor.gif) ![1.editor.gif](./quickstart/1.editor.gif)
点击添加菜单项,添加菜单分组 “Customers” 和 “Orders & Products” ,然后添加子菜单页面 “All Orders” 和 “Products”。 点击添加菜单项,添加菜单分组 “Customers” 和 “Orders & Products” ,然后添加子菜单页面 “All Orders” 和 “Products”。
![1.menu.gif](./5-minutes-to-get-started/1.menu.gif) ![1.menu.gif](./quickstart/1.menu.gif)
添加完菜单和页面之后,我们可以在页面内添加和配置区块了。 添加完菜单和页面之后,我们可以在页面内添加和配置区块了。
@ -75,16 +75,16 @@ NocoBase 目前支持表格、看板、日历、表单、详情等类型的区
我们在“所有订单”页面,添加一个表格区块,数据源选择 Collection `Orders` ,并为这个表格区块配置需要显示的列。 我们在“所有订单”页面,添加一个表格区块,数据源选择 Collection `Orders` ,并为这个表格区块配置需要显示的列。
![1.block.gif](./5-minutes-to-get-started/1.block.gif) ![1.block.gif](./quickstart/1.block.gif)
给这个表格区块配置操作,包括筛选、添加、删除、查看、编辑。 给这个表格区块配置操作,包括筛选、添加、删除、查看、编辑。
![1.action.gif](./5-minutes-to-get-started/1.action.gif) ![1.action.gif](./quickstart/1.action.gif)
为新增、编辑、查看等操作配置表单和详情区块。 为新增、编辑、查看等操作配置表单和详情区块。
![1.action-block.gif](./5-minutes-to-get-started/1.action-block.gif) ![1.action-block.gif](./quickstart/1.action-block.gif)
然后,用同样的方法,在 Products 和 Customers 页面布置表格区块。完成后,退出界面配置模式,进入使用模式,一个简单的订单管理系统就完成了。 然后,用同样的方法,在 Products 和 Customers 页面布置表格区块。完成后,退出界面配置模式,进入使用模式,一个简单的订单管理系统就完成了。
![1.finished.gif](./5-minutes-to-get-started/1.finished.gif) ![1.finished.gif](./quickstart/1.finished.gif)

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 715 KiB

View File

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 354 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 335 KiB

View File

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 499 KiB

View File

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 499 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 429 KiB

After

Width:  |  Height:  |  Size: 429 KiB

View File

Before

Width:  |  Height:  |  Size: 990 KiB

After

Width:  |  Height:  |  Size: 990 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 410 KiB

View File

Before

Width:  |  Height:  |  Size: 803 KiB

After

Width:  |  Height:  |  Size: 803 KiB

View File

Before

Width:  |  Height:  |  Size: 803 KiB

After

Width:  |  Height:  |  Size: 803 KiB

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