diff --git a/docs/APIServer.png b/docs/APIServer.png
deleted file mode 100644
index 0e01b14966..0000000000
Binary files a/docs/APIServer.png and /dev/null differ
diff --git a/docs/AdminServer.png b/docs/AdminServer.png
deleted file mode 100644
index 5e3a8a6821..0000000000
Binary files a/docs/AdminServer.png and /dev/null differ
diff --git a/docs/MiniMISServer.png b/docs/MiniMISServer.png
deleted file mode 100644
index 5b07372bd1..0000000000
Binary files a/docs/MiniMISServer.png and /dev/null differ
diff --git a/docs/MiniProgramServer.png b/docs/MiniProgramServer.png
deleted file mode 100644
index 6b0699fa42..0000000000
Binary files a/docs/MiniProgramServer.png and /dev/null differ
diff --git a/docs/SaaSServer1.png b/docs/SaaSServer1.png
deleted file mode 100644
index a16fc7abb9..0000000000
Binary files a/docs/SaaSServer1.png and /dev/null differ
diff --git a/docs/SaaSServer2.png b/docs/SaaSServer2.png
deleted file mode 100644
index d8c3e803bd..0000000000
Binary files a/docs/SaaSServer2.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index 7e4a58d0f5..421974da39 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,17 +1,17 @@
---
-title: 介绍
+title: NocoBase
toc: menu
---
# NocoBase
-考虑到大家是初次接触 NocoBase,开发文档的第一篇,先带大家了解基础概念。NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。
+NocoBase 采用微内核架构,框架只保留核心,各类功能以插件形式扩展。
## 微服务 - Microservices
-首先我们创建一个应用,新建一个 app.js 文件,代码如下:
+为了更快的理解 NocoBase,我们先创建一个应用,新建一个 app.js 文件,代码如下:
```ts
const { Application } = require('@nocobase/server');
@@ -23,10 +23,10 @@ const app = new Application({
// 配置一张 users 表
app.collection({
name: 'users',
- schema: {
- username: 'string',
- password: 'password',
- },
+ schema: [
+ { type: 'string', name: 'username' },
+ { type: 'password', name: 'password' }
+ ],
});
// 解析 argv 参数,终端通过命令行进行不同操作
@@ -37,7 +37,7 @@ app.parse(process.argv);
```bash
# 根据配置生成数据库表结构
-node app.js db sync
+node app.js db:sync
# 启动应用
node app.js start --port=3000
```
@@ -113,18 +113,21 @@ await api.resource('users').logout();
NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些必要的 API,这里列一些重点:
- `app.db`:数据库实例,每个 app 都有自己的 db。
- - `db.getTable()` 数据表/数据集配置
- - `db.getRepository()` 数据仓库
- - `db.getModel()` 数据模型
+ - `db.getCollection()` 数据表/数据集
+ - `collection.schema` 数据结构
+ - `collection.repository` 数据仓库
+ - `collection.model` 数据模型
- `db.on()` 添加事件监听,由 EventEmitter 提供
- `db.emit()` 触发事件,由 EventEmitter 提供
- `db.emitAsync()` 触发异步事件
-- `app.cli`,commander 实例,提供命令行操作
+- `app.cli`,Commander 实例,提供命令行操作
- `app.context`,上下文
- `ctx.db`
- - `ctx.action`
+ - `ctx.action` 当前资源操作实例
+ - `action.params` 操作参数
+ - `action.mergeParams()` 参数合并方法
- `app.constructor()` 初始化
-- `app.collection()` 定义数据 Schema,等同于 `app.db.table()`
+- `app.collection()` 定义数据 Schema,等同于 `app.db.collection()`
- `app.resource()` 定义资源
- `app.actions()` 定义资源的操作方法
- `app.on()` 添加事件监听,由 EventEmitter 提供
@@ -136,8 +139,6 @@ NocoBase 的 Application 继承了 Koa,集成了 DB 和 CLI,添加了一些
- `app.load()` 载入配置,主要用于载入插件
- `app.parse()` 解析 argv 参数,写在最后,等同于 `app.cli.parseAsync()`
-经过几次改进,以上罗列的 API 趋近于稳定,但也可能有所变动。
-
## 数据集 - Collection
NocoBase 通过 `app.collection()` 方法定义数据的 Schema,Schema 的类型包括:
@@ -175,17 +176,9 @@ NocoBase 通过 `app.collection()` 方法定义数据的 Schema,Schema 的类
app.collection({
name: 'users',
schema: {
- username: {
- type: 'string',
- unique: true,
- },
- password: {
- type: 'password',
- unique: true,
- },
- posts: {
- type: 'hasMany',
- },
+ username: { type: 'string', unique: true },
+ password: { type: 'password', unique: true },
+ posts: { type: 'hasMany' },
},
});
@@ -193,14 +186,11 @@ app.collection({
app.collection({
name: 'posts',
schema: {
- title: 'string',
- content: 'text',
- tags: 'belongsToMany',
+ title: 'string',
+ content: 'text',
+ tags: 'belongsToMany',
comments: 'hasMany',
- author: {
- type: 'belongsTo',
- target: 'users',
- },
+ author: { type: 'belongsTo', target: 'users' },
},
});
@@ -225,13 +215,6 @@ app.collection({
除了通过 `app.collection()` 配置 schema,也可以直接调用 api 插入或修改 schema,collection 的核心 API 有:
-- `collection.model` 当前 collection 的数据模型
-- `collection.repository` 当前 collection 的数据仓库
- - `repository.findAll()`
- - `repository.findOne()`
- - `repository.create()`
- - `repository.update()`
- - `repository.destroy()`
- `collection.schema` 当前 collection 的数据结构
- `schema.has()` 判断是否存在
- `schema.get()` 获取
@@ -239,8 +222,15 @@ app.collection({
- `schema.merge()` 添加、或指定 key path 替换
- `schema.replace()` 替换
- `schema.delete()` 删除
+- `collection.repository` 当前 collection 的数据仓库
+ - `repository.findAll()`
+ - `repository.findOne()`
+ - `repository.create()`
+ - `repository.update()`
+ - `repository.destroy()`
+- `collection.model` 当前 collection 的数据模型
-如:
+Schema 示例:
```ts
const collection = app.db.getCollection('posts');
@@ -269,7 +259,7 @@ collection.schema.merge({
await collection.sync();
```
-存在外键关联时,也无需顾虑建表和字段的顺序,`db sync` 时会自动处理。`db sync` 之后,就可以往表里写入数据了。可以使用 Repository 或 Model 操作。
+`db:sync` 是非常常用的命令行之一,数据库根据 collection 的 schema 生成表结构。更多详情见 CLI 章节。`db:sync` 之后,就可以往表里写入数据了,可以使用 Repository 或 Model 操作。
- Repository 初步提供了 findAll、findOne、create、update、destroy 核心操作方法。
- Model 为 Sequelize.Model,详细使用说明可以查看 Sequelize 文档。
@@ -344,7 +334,7 @@ await user.updateAssociations({
Resource 是互联网资源,互联网资源都对应一个地址。客户端请求资源地址,服务器响应请求,在这里「请求」就是一种「操作」,在 REST 里通过判断请求方法(GET/POST/PUT/DELETE)来识别具体的操作,但是请求方法局限性比较大,如上文提到的登录、注册、注销就无法用 REST API 的方式表示。为了解决这类问题,NocoBase 以 `:` 格式表示资源的操作。在关系模型的世界里,关系无处不在,基于关系,NocoBase 又延伸了关系资源的概念,对应关系资源的操作的格式为 `.:`。
-Collection 会自动同步给 Resource,上文 Collection 章节定义的 Schema,提炼的资源有:
+Collection 会自动同步给 Resource,如上文 Collection 章节定义的 Schema,可以提炼的资源有:
- `users`
- `users.posts`
@@ -492,7 +482,7 @@ async function (ctx, next) {
}
```
-多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章
+`ctx.action.mergeParams()` 主要用于多来源参数合并,以 `filter` 参数为例。如:客户端请求日期 2021-09-15 创建的文章
```bash
GET /api/posts:list?filter={"created_at": "2021-09-15"}
@@ -533,6 +523,7 @@ app.use(async (ctx, next) => {
async function list(ctx, next) {
// list 操作中获取到的 filter
console.log(ctx.params.filter);
+ // filter 是特殊的 and 合并
// {
// and: [
// { created_at: '2021-09-15' },
@@ -576,27 +567,31 @@ app.use(async (ctx, next) => {
});
```
-弥补 `app.use()` 不足,加了个 `middleware()` 适配器,可以用于限定 resource 和 action。除此之外,也可以控制中间件的插入位置。
+与 `koa.use(middleware)` 略有不同,`app.use(middleware, options)` 多了个 options 参数,可以用于限定 resource 和 action,也可以用于控制中间件的插入位置。
```ts
import { middleware } from '@nocobase/server';
-app.use(middleware(async (ctx, next) => {}, {
+app.use(async (ctx, next) => {}, {
name: 'middlewareName1',
- resourceNames: [],
- actionNames: [],
+ resourceNames: [], // 作用于资源内所有 actions
+ actionNames: [
+ 'list', // 全部 list action
+ 'users:list', // 仅 users 资源的 list action,
+ ],
insertBefore: '',
insertAfter: '',
-}));
+});
```
## 命令行 - CLI
-Application 除了可以做 HTTP Server 以外,也可以是 CLI(内置了 Commander)。目前内置的命令有:
+Application 除了可以做 HTTP Server 以外,也是 CLI(内置了 Commander)。目前内置的命令有:
-- `db sync --force` 用于配置与数据库表结构同步
+- `init` 初始化
+- `db:sync --force` 用于配置与数据库表结构同步
- `start --port` 启动应用
-- `plugin` 插件相关
+- `plugin:**` 插件相关
自定义:
@@ -624,10 +619,8 @@ app.command('foo').action(async () => {
- CLI
- `app.cli` commander 实例
- `app.command()` 等同于 `app.cli.command()`
-- Plugin
- - `app.plugin` 添加插件
-基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。完整的插件包括安装、升级、激活、载入、禁用、卸载流程,但是并不是所有插件都要这完整的流程。比如:
+基于以上扩展接口,进一步提供了模块化、可插拔的插件,可以通过 `app.plugin()` 添加。插件的流程包括安装、升级、激活、载入、禁用、卸载,不需要的流程可缺失。如:
**最简单的插件**
@@ -709,10 +702,10 @@ app.plugin('@nocobase/plugin-action-logs');
**插件 CLI**
```bash
-plugin install pluginName1
-plugin unstall pluginName1
-plugin activate pluginName1
-plugin deactivate pluginName1
+plugin:install pluginName1
+plugin:unstall pluginName1
+plugin:activate pluginName1
+plugin:deactivate pluginName1
```
目前已有的插件:
diff --git a/examples/saas/src/index.ts b/examples/saas/src/index.ts
index 2f705c6c2f..340bc3bb35 100644
--- a/examples/saas/src/index.ts
+++ b/examples/saas/src/index.ts
@@ -1,16 +1,12 @@
import { Application } from '@nocobase/server/src';
import path from 'path';
-import mount from 'koa-mount';
import compose from 'koa-compose';
const keys = __dirname.split(path.sep);
const slug = keys[keys.length - 2];
-const apps = new Map();
-
function createApp(opts) {
- const { name, prefix } = opts;
-
+ const { name } = opts;
const options = {
database: {
username: process.env.DB_USER,
@@ -23,28 +19,27 @@ function createApp(opts) {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
+ appName: name,
hooks: {
beforeDefine(model, options) {
- options.tableName = `examples_${slug}_${name}_${options.tableName || options.name.plural}`;
+ options.tableName = `examples_${slug}_${name}_${
+ options.tableName || options.name.plural
+ }`;
},
},
},
resourcer: {
- prefix,
+ prefix: `/api/examples/${slug}/${name}`,
},
};
- console.log(options);
const app = new Application(options);
- if (name) {
- apps.set(name, app);
- }
app.resource({
- name: 'server',
+ name: 'saas',
actions: {
async getInfo(ctx, next) {
- ctx.body = name;
+ ctx.body = ctx.db.options;
await next();
- }
+ },
},
});
app.collection({
@@ -57,78 +52,103 @@ function createApp(opts) {
return app;
}
-const app = createApp({
+const saas = createApp({
name: 'main',
- prefix: `/api/examples/${slug}/main`
});
-app.collection({
+saas['apps'] = new Map();
+
+saas.collection({
name: 'applications',
fields: [
{ type: 'string', name: 'name', unique: true },
],
});
-app.command('app-create').argument('').action(async (appName) => {
- const App = app.db.getModel('applications');
- const server = await App.create({
- name: appName,
+saas
+ .command('app:create')
+ .argument('')
+ .action(async (appName) => {
+ const App = saas.db.getModel('applications');
+ const model = await App.create({
+ name: appName,
+ });
+ const app = createApp({
+ name: appName,
+ });
+ await app.db.sync();
+ await app.destroy();
+ await saas.destroy();
+ console.log(model.toJSON());
});
- const api = createApp({
- name: appName,
- prefix: `/api/examples/${slug}/${appName}`,
- });
- await api.db.sync();
- await api.destroy();
- console.log(server.toJSON());
- await app.destroy();
-});
-app.command('dbsync')
+saas
+ .command('db:sync')
.option('-f, --force')
.option('--app [app]')
.action(async (...args) => {
const cli = args.pop();
const force = cli.opts()?.force;
const appName = cli.opts()?.app;
- console.log('ac ac', cli.opts());
- const api = apps.get(appName) || app;
- await api.load();
- await api.db.sync(
+ const app = !appName
+ ? saas
+ : createApp({
+ name: appName,
+ });
+ await app.load();
+ await app.db.sync(
force
? {
- force: true,
- alter: {
- drop: true,
- },
- }
+ force: true,
+ alter: {
+ drop: true,
+ },
+ }
: {},
);
- await api.destroy();
+ await app.destroy();
+ await saas.destroy();
});
-app.use(async function(ctx, next) {
- const appName = ctx.path.split('/')[4];
- if (appName === 'main') {
- return next();
- }
- const App = ctx.db.getModel('applications');
- const model = await App.findOne({
- where: { name: appName },
- });
- console.log({ appName, model })
- if (!model) {
- return next();
- }
- if (!apps.has(appName)) {
- const app1 = createApp({
- name: appName,
- prefix: `/api/examples/${slug}/${appName}`
+function multiApps({ getAppName }) {
+ return async function (ctx, next) {
+ const appName = getAppName(ctx);
+ if (!appName) {
+ return next();
+ }
+ const App = ctx.db.getModel('applications');
+ const model = await App.findOne({
+ where: { name: appName },
});
- apps.set(appName, app1);
- }
- const server = apps.get(appName);
- await compose(server.middleware)(ctx, next);
-});
+ console.log({ appName, model });
+ if (!model) {
+ return next();
+ }
+ const apps = ctx.app.apps;
+ if (!apps.has(appName)) {
+ const app = createApp({
+ name: appName,
+ });
+ apps.set(appName, app);
+ }
+ const saas = apps.get(appName);
+ await compose(saas.middleware)(ctx, async () => {});
+ };
+}
-app.parse(process.argv);
+saas.use(
+ multiApps({
+ getAppName(ctx) {
+ const appName = ctx.path.split('/')[4];
+ return appName === 'main' ? null : appName;
+ },
+ }),
+);
+
+// saas.use(async (ctx, next) => {
+// ctx.body = 'aaaaa';
+// console.log(ctx.db.options);
+// await next();
+// });
+
+saas.parse(process.argv);
diff --git a/packages/server/src/application.ts b/packages/server/src/application.ts
index aae902680f..22a0767468 100644
--- a/packages/server/src/application.ts
+++ b/packages/server/src/application.ts
@@ -20,7 +20,34 @@ export interface ApplicationOptions {
dataWrapping?: boolean;
}
-export class Application extends Koa {
+interface DefaultState {
+ currentUser?: any;
+ [key: string]: any;
+}
+
+interface DefaultContext {
+ db: Database;
+ resourcer: Resourcer;
+ [key: string]: any;
+}
+
+interface MiddlewareOptions {
+ name?: string;
+ resourceName?: string;
+ resourceNames?: string[];
+ insertBefore?: string;
+ insertAfter?: string;
+}
+
+interface ActionsOptions {
+ resourceName?: string;
+ resourceNames?: string[];
+}
+
+export class Application<
+ StateT = DefaultState,
+ ContextT = DefaultContext
+ > extends Koa {
public readonly db: Database;
@@ -55,7 +82,7 @@ export class Application extends Koa {
}),
);
- this.use(async (ctx, next) => {
+ this.use(async (ctx, next) => {
ctx.db = this.db;
ctx.resourcer = this.resourcer;
await next();
@@ -71,7 +98,7 @@ export class Application extends Koa {
registerActions(this);
this.cli
- .command('db sync')
+ .command('db:sync')
.option('-f, --force')
.action(async (...args) => {
console.log('db sync...');
@@ -92,7 +119,7 @@ export class Application extends Koa {
});
this.cli
- .command('db init')
+ .command('init')
// .option('-f, --force')
.action(async (...args) => {
const cli = args.pop();
@@ -121,6 +148,15 @@ export class Application extends Koa {
});
}
+ // @ts-ignore
+ use(
+ middleware: Koa.Middleware,
+ options?: MiddlewareOptions,
+ ): Application {
+ // @ts-ignore
+ return super.use(middleware);
+ }
+
collection(options: TableOptions) {
return this.db.table(options);
}
@@ -129,7 +165,7 @@ export class Application extends Koa {
return this.resourcer.define(options);
}
- actions(handlers: any) {
+ actions(handlers: any, options?: ActionsOptions) {
return this.resourcer.registerActions(handlers);
}