docs: relation repository & acl (#848)

* docs: relation-repository

* docs: has many repository

* docs: acl

* docs: acl

* docs: acl

* docs: acl

* docs: acl/AllowManager

* docs: acl/ACLAvailableAction

* docs: acl

* docs: clean up

* feat: doc menus

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
ChengLei Shao 2022-10-06 10:29:53 +08:00 committed by GitHub
parent 460dbcbc7f
commit d805fafbfc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 913 additions and 154 deletions

View File

@ -55,7 +55,7 @@ export default {
'/development/guide/i18n', '/development/guide/i18n',
'/development/guide/migration', '/development/guide/migration',
{ {
title: 'UI Schema Designer', title: 'UI 设计器',
type: 'subMenu', type: 'subMenu',
children: [ children: [
// '/development/guide/ui-schema-designer/index', // '/development/guide/ui-schema-designer/index',
@ -72,7 +72,6 @@ export default {
}, },
'/development/guide/ui-router', '/development/guide/ui-router',
'/development/guide/settings-center', '/development/guide/settings-center',
'/development/guide/commands',
], ],
}, },
{ {
@ -177,7 +176,10 @@ export default {
'/api/database/collection', '/api/database/collection',
'/api/database/field', '/api/database/field',
'/api/database/repository', '/api/database/repository',
'/api/database/relation-repository', '/api/database/relation-repository/has-one-repository',
'/api/database/relation-repository/has-many-repository',
'/api/database/relation-repository/belongs-to-repository',
'/api/database/relation-repository/belongs-to-many-repository',
'/api/database/operators', '/api/database/operators',
], ],
}, },
@ -192,8 +194,17 @@ export default {
], ],
}, },
{ {
title: '@nocobase/actions', title: '@nocobase/acl',
path: '/api/actions', type: 'subMenu',
children: [
'/api/acl/index',
'/api/acl/acl',
'/api/acl/acl-role',
'/api/acl/acl-resource',
'/api/acl/acl-available-action',
'/api/acl/acl-available-strategy',
'/api/acl/allow-manager',
],
}, },
{ {
title: '@nocobase/client', title: '@nocobase/client',
@ -225,14 +236,14 @@ export default {
}, },
], ],
}, },
{
title: '@nocobase/acl',
path: '/api/acl',
},
{ {
title: '@nocobase/cli', title: '@nocobase/cli',
path: '/api/cli', path: '/api/cli',
}, },
{
title: '@nocobase/actions',
path: '/api/actions',
},
{ {
title: '@nocobase/sdk', title: '@nocobase/sdk',
path: '/api/sdk', path: '/api/sdk',

View File

@ -1 +0,0 @@
# ACL

View File

@ -0,0 +1,19 @@
# ACLAvailableAction
用于表示一个可用 ACL Action 的数据结构。
## 类方法
### `constructor(public name: string, public options: AvailableActionOptions)`
实例化 ACLAvailableAction
**参数**
* name: string - 动作名称
* options: AvailableActionOptions
* displayName - action 显示名称
* aliases - action 别名
* resource - action 所属资源名称
* onNewRecord - action 是否是在创建新的数据库记录
* allowConfigureFields - action 是否允许配置字段

View File

@ -0,0 +1,24 @@
# ACLAvailableStrategy
ACL 角色的权限策略,可以使用其判断角色是否有权限访问资源。
## 类方法
### `constructor(acl: ACL, options: AvailableStrategyOptions)`
构造函数,创建一个 `ACLAvailableStrategy` 实例。
### `allow(resourceName: string, actionName: string)`
判断此策略是否允许给定的资源和动作通过鉴权。
## 基础数据结构
### `AvailableStrategyOptions`
策略定义参数,用以描述一组权限配置规则。
* displayName - 策略名称
* allowConfigure - 此策略是否拥有 **配置资源** 的权限,设置此项为`true`之后,请求判断在 `ACL` 中注册成为 `configResources` 资源的权限,会返回通过。
* actions - 策略内的 actions 列表,支持通配符 `*`
* resource - 策略内的 resource 定义,支持通配符 `*`

View File

@ -0,0 +1,57 @@
# ACLResource
ACLResourceACL 系统中的资源类。在 ACL 系统中,为用户授予权限时会自动创建对应的资源。
## 基础数据结构
### `ResourceActions`
Action 集合对象:
* key 表示 action 的名称
* value 表示 action 的配置参数,见 [`RoleActionParams`](#RoleActionParams)。
**定义**
```typescript
type ResourceActions = { [key: string]: RoleActionParams };
```
## 类方法
### `constructor(options: AclResourceOptions)`
创建 `ACLResource` 实例
**AclResourceOptions 参数**
* options - 资源配置参数
* name - 资源名称
* role - 资源所属角色
* actions - ResourceActions 对象,定义资源的 Action
### `getActions()`
获取资源的所有 Action返回结果为 `ResourceActions` 对象。
### `getAction(name: string)`
根据名称返回 Action 的参数配置,返回结果为 `RoleActionParams` 对象。
## `setAction(name: string, params: RoleActionParams)`
在资源内部设置一个 Action 的参数配置,返回结果为 `RoleActionParams` 对象。
**参数**
* name - 要设置的 action 名称
* params - [`RoleActionParams`](#RoleActionParams)
## `setActions(actions: ResourceActions)`
批量调用 `setAction` 的便捷方法
**参数**
* actions: [RoleActionParams](#RoleActionParams)

View File

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

214
docs/zh-CN/api/acl/acl.md Normal file
View File

@ -0,0 +1,214 @@
# ACL
ACL 为权限管理类,系统中的角色与资源都可以在 ACL 中进行注册。
## 成员变量
### `availableActions: Map<string, AclAvailableAction>`
ACL 内的 `AclAvailableAction` 名称映射。
### `availableStrategy: Map<string, ACLAvailableStrategy>`
ACL 内的 [`ACLAvailableStrategy`](#ACLAvailableStrategy) 名称映射。
### `middlewares`
ACL 鉴权中间件。
### `roles: Map<string, ACLRole>`
ACL 内的 `ACLRole` 名称映射。
### `actionAlias: Map<string, string>`
Action 别名映射。
### `configResources: Array<string>`
配置资源列表。
## 类方法
### `constructor()`
构造函数,创建一个 `ACL` 实例。
```typescript
import { ACL } from '@nocobase/database';
const acl = new ACL();
```
### `define(options: DefineOptions)`
定义系统角色
**DefineOptions 参数**
* `role` - 角色名称
```typescript
// 定义一个名称为 admin 的角色
acl.define({
role: 'admin',
});
```
* `allowConfigure` - 是否允许配置权限
* `strategy` - 角色的权限策略
* 可以为 `string`,为要使用的策略名,表示使用已定义的策略。
* 可以为 `AvailableStrategyOptions`,为该角色定义一个新的策略。
* `actions` - 定义角色时,可传入角色可访问的 `actions` 对象,
之后会依次调用 `aclRole.grantAction` 授予资源权限。详见 [`ResourceActionsOptions`](#ResourceActionsOptions)
```typescript
acl.define({
role: 'admin',
actions: {
'posts:edit': {}
},
});
// 等同于
const role = acl.define({
role: 'admin',
});
role.grantAction('posts:edit', {});
```
### `getRole(name: string): ACLRole`
根据角色名称返回角色对象
### `removeRole(name: string)`
根据角色名称移除角色
### `can({ role, resource, action }: CanArgs): CanResult | null`
鉴权函数,调用返回为`null`时,表示角色无权限,反之返回`CanResult`对象,表示角色有权限。
`can` 方法首先会判断角色是否有注册对应的 `Action` 权限,如果没有则会去判断角色的 `strategy` 是否匹配.
**CanArgs 参数**
* role - 角色名称
* resource - 资源名称
* action - 操作名称
**CanResult 参数**
* role - 角色名称
* resource - 资源名称
* action - 操作名称
* params - 注册权限时传入的参数
```typescript
acl.define({
role: 'admin',
actions: {
'posts:edit': {
fields: ['title', 'content'],
},
},
});
const canResult = acl.can({
role: 'admin',
resource: 'posts',
action: 'edit',
});
/**
* canResult = {
* role: 'admin',
* resource: 'posts',
* action: 'edit',
* params: {
* fields: ['title', 'content'],
* }
* }
*/
acl.can({
role: 'admin',
resource: 'posts',
action: 'destroy',
}); // null
```
### `use(fn: any)`
向 middlewares 中添加中间件函数。
### `middleware()`
返回一个中间件函数,用于在 `@nocobase/server` 中使用。使用此 `middleware` 之后,`@nocobase/server` 在每次请求处理之前都会进行权限判断。
### `setAvailableStrategy(name: string, options: AvailableStrategyOptions)`
注册一个可用的权限策略
### `registerConfigResource(name: string)`
将传入的资源名称设置为**配置资源**。配置资源是指这样的一种资源,这些资源的改动会调用`ACL`中的角色、权限注册相关方法,例如用户、权限、角色等,这些资源就需要被设置为配置资源。
### `registerConfigResources(names: string[])`
`registerConfigResource` 的批量方法
### `isConfigResource(name: string)`
判断传入的资源名称是否为配置资源
### `setAvailableAction(name: string, options: AvailableActionOptions = {})`
设置 ACL 中有效的 Action 名称。
**参数**
* name - action 名称
* options - action 选项
* displayName - action 显示名称
* aliases - action 别名
* resource - action 所属资源名称
* onNewRecord - action 是否是在创建新的数据库记录
* allowConfigureFields - action 是否允许配置字段
### `getAvailableAction(name: string)`
获取 ACL 中有效的 Action。
### `setAvailableStrategy(name: string, options: AvailableStrategyOptions)`
设置可用的权限策略,详见 [`AvailableStrategyOptions`](#AvailableStrategyOptions)
### `allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc)`
在不指定角色的情况下,开放资源的访问权限。
举例来说,例如登录操作,可以被公开访问:
```typescript
// 注册 users:login 可以被公开访问
acl.allow('users', 'login');
```
**参数**
* resourceName - 资源名称
* actionNames - 资源动作名
* condition? - 配置生效条件
* 传入 `string`,表示使用已定义的条件,注册条件使用 `acl.allowManager.registerCondition` 方法。
```typescript
acl.allowManager.registerAllowCondition('superUser', async () => {
return ctx.state.user?.id === 1;
});
// 开放 users:list 的权限,条件为 superUser
acl.allow('users', 'list', 'superUser');
```
* 传入 ConditionFunc可接收 `ctx` 参数,返回 `boolean`,表示是否生效。
```typescript
// 当用户ID为1时可以访问 user:list
acl.allow('users', 'list', (ctx) => {
return ctx.state.user?.id === 1;
});
```

View File

@ -0,0 +1,55 @@
# AllowManager
开放权限管理
## 类方法
### `constructor(public acl: ACL)`
实例化 AllowManger
### `allow(resourceName: string, actionName: string, condition?: string | ConditionFunc)`
注册开放权限
```typescript
// users:login 可以被公开访问
allowManager.allow('users', 'login');
```
**参数**
* resourceName - 资源名称
* actionName - 资源动作名
* condition? - 配置生效条件
* 传入 `string`,表示使用已定义的条件,注册条件使用 `acl.allowManager.registerCondition` 方法。
```typescript
acl.allowManager.registerAllowCondition('superUser', async () => {
return ctx.state.user?.id === 1;
});
// 开放 users:list 的权限,条件为 superUser
acl.allow('users', 'list', 'superUser');
```
* 传入 ConditionFunc可接收 `ctx` 参数,返回 `boolean`,表示是否生效。
```typescript
// 当用户ID为1时可以访问 user:list
acl.allow('users', 'list', (ctx) => {
return ctx.state.user?.id === 1;
});
```
### `registerAllowCondition(name: string, condition: ConditionFunc)`
注册开放权限条件
### `getAllowedConditions(resourceName: string, actionName: string): Array<ConditionFunc | true>`
获取已注册的开放条件
**参数**
* resourceName - 资源名称
* actionName - 资源动作名
### `aclMiddleware()`
中间件,注入于 `acl` 实例中,用于判断是否开放权限,若根据条件判断为开放权限,则在 `acl` middleware 中会跳过权限检查。

View File

@ -0,0 +1,12 @@
# ACL 权限管理
ACL 为 Nocobase 中的权限控制模块。在 ACL 中注册角色、资源以及配置相应权限之后,即可对角色进行权限判断。
## 概念解释
* 角色 (`ACLRole`):权限判断的对象
* 资源 (`ACLResource`):在 Nocobase ACL 中,资源通常对应一个数据库表,概念上可类比为 Restful API 中的 Resource。
* Action对资源的操作`create`、`read`、`update`、`delete` 等。
* 策略 (`ACLAvailableStrategy`): 通常每个角色都有自己的权限策略,策略中定义了默认情况下的用户权限。
* 授权:在 `ACLRole` 实例中调用 `grantAction` 函数,为角色授予 `Action` 的访问权限。
* 鉴权:在 `ACL` 实例中调用 `can` 函数,函数返回结果既为用户的鉴权结果。

View File

@ -1,87 +0,0 @@
# 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,23 @@
## BelongsToManyRepository
### `count()`
### `find()`
### `findOne()`
### `findAndCount()`
### `create()`
### `update()`
### `destroy()`
### `add()`
### `remove()`
### `set()`
### `toggle()`

View File

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

View File

@ -0,0 +1,152 @@
# HasManyRepository
`HasManyRepository` 是用于处理 `HasMany` 关系的 `Repository`
## 类方法
### `find()`
查找关联对象
**签名**
* `async find(options?: FindOptions): Promise<M[]>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `FindOptions` | - | 参见 repository.find |
### `findOne()`
查找关联对象,仅返回一条记录
**签名**
* `async findOne(options?: FindOneOptions): Promise<M>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `FindOneOptions` | - | 参见 repository.findOne |
### `count()`
返回符合查询条件的记录数
**签名**
* `async count(options?: CountOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `CountOptions` | - | 参见 repository.count |
### `findAndCount()`
同时返回符合查询条件的记录集合与记录数
**签名**
* `async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `FindAndCountOptions` | - | 参见 repository.findAndCount |
### `create()`
创建关联对象
**签名**
* `async create(options?: CreateOptions): Promise<M>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `CreateOptions` | - | 参见 repository.create |
### `update()`
更新符合条件的关联对象
**签名**
* `async update(options?: UpdateOptions): Promise<M>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `UpdateOptions` | - | 参见 repository.update |
### `destroy()`
删除符合条件的关联对象
**签名**
* `async destroy(options?: TK | DestroyOptions): Promise<M>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `TK \|DestroyOptions` | - | 传入删除对象的 `targetKeyId`,或者 `targetKeyId` 数组。需传 `transaction` 时 使用 `DestroyOptions` 类型 |
### `add()`
添加关联对象
**签名**
* `async add(options: TargetKey | TargetKey[] | AssociatedOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `TargetKey` | - | 关联对象的 `targetKeyId` |
| `options` | `TargetKey[]` | - | 多个关联对象的 `targetKeyId` 数组 |
| `options` | `AssociatedOptions` | - | `options.tk`, 为 `targetKeyId` 或者 `targetKeyId` 数组;`options.transaction`,为 `Transaction` 对象 |
### `remove()`
移除符合条件的关联对象
**签名**
* `async remove(options: TargetKey | TargetKey[] | AssociatedOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `TargetKey \| TargetKey[] \| AssociatedOptions` | - | 同 [add](#add) |
### `set()`
设置当前关系的关联对象
**签名**
* `async set(options: TargetKey | TargetKey[] | AssociatedOptions)`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `TargetKey \| TargetKey[] \| AssociatedOptions` | - | 同 [add](#add) |

View File

@ -0,0 +1,180 @@
# HasOneRepository
`HasOneRepository``HasOne` 类型的关联 Repository。
```typescript
const User = db.collection({
name: 'users',
fields: [
{ type: 'hasOne', name: 'profile' },
{ type: 'string', name: 'name' },
],
});
const Profile = db.collection({
name: 'profiles',
fields: [{ type: 'string', name: 'avatar' }],
});
const user = await User.repository.create({
values: { name: 'u1' },
});
// 创建 HasOneRepository 实例
const userProfileRepository = new HasOneRepository(User, 'profile', user.get('id'));
```
## 类方法
### `create(options?: CreateOptions)`
创建关联对象
**签名**
* `async create(options?: CreateOptions): Promise<Model>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `CreateOptions` | - | 参见 repository.create |
**示例**
```typescript
const profile = await UserProfileRepository.create({
values: { avatar: 'avatar1' },
});
console.log(profile.toJSON());
/*
{
id: 1,
avatar: 'avatar1',
userId: 1,
updatedAt: 2022-09-24T13:59:40.025Z,
createdAt: 2022-09-24T13:59:40.025Z
}
*/
```
### `find()`
查找关联对象
**签名**
* `async find(options?: SingleRelationFindOption): Promise<Model<any> | null>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.fields` | `Fields` | - | 参见 repository.find.fields |
| `options.except` | `Except` | - | 参见 repository.find.except |
| `options.appends` | `Appends` | - | 参见 repository.find.appends |
| `options.filter` | `Filter` | - | 参见 repository.find.filter |
**示例**
```typescript
const profile = await UserProfileRepository.find();
// 关联对象不存在时,返回 null
```
### `update()`
更新关联对象
**签名**
* `async update(options: UpdateOptions): Promise<Model>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | `UpdateOptions` | - | 参见 repository.update |
**示例**
```typescript
const profile = await UserProfileRepository.update({
values: { avatar: 'avatar2' },
});
profile.get('avatar'); // 'avatar2'
```
### `remove()`
移除关联对象,仅解除关联关系,不删除关联对象
**签名**
* `async remove(options?: Transactionable): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.transaction` | `Transaction` | - | Transaction |
**示例**
```typescript
await UserProfileRepository.remove();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 1; // true
```
### `destroy()`
删除关联对象
**签名**
* `async destroy(options?: Transactionable): Promise<Boolean>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options.transaction` | `Transaction` | - | Transaction |
**示例**
```typescript
await UserProfileRepository.destroy();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 0; // true
```
### `set()`
设置关联对象
**签名**
* `async set(options: TargetKey | SetOption): Promise<void>`
**参数**
| 参数名 | 类型 | 默认值 | 描述 |
| --- | --- | --- | --- |
| `options` | ` TargetKey \| SetOption` | - | 需要 set 的对象的 targetKey如果需要一同传入 transaction 则修改为 object 类型参数 |
**示例**
```typescript
const newProfile = await Profile.repository.create({
values: { avatar: 'avatar2' },
});
await UserProfileRepository.set(newProfile.get('id'));
(await UserProfileRepository.find()).get('id') === newProfile.get('id'); // true
```

View File

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

View File

@ -1,4 +1,4 @@
# Middleware # 中间件
## 添加方法 ## 添加方法

View File

@ -1,4 +1,4 @@
# Settings Center # 配置中心
<img src="./settings-tab.jpg" style="max-width: 100%;"/> <img src="./settings-tab.jpg" style="max-width: 100%;"/>

View File

@ -1,4 +1,4 @@
# UI Router # UI 路由
NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `<RouteSwitch routes={[]} />` 来配置 ui routes例子如下 NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `<RouteSwitch routes={[]} />` 来配置 ui routes例子如下

View File

@ -38,6 +38,7 @@ describe('acl', () => {
}, },
}); });
}); });
it('should define role with predicate', () => { it('should define role with predicate', () => {
acl.setAvailableAction('edit', { acl.setAvailableAction('edit', {
type: 'old-data', type: 'old-data',

View File

@ -12,6 +12,6 @@ export interface AvailableActionOptions {
allowConfigureFields?: boolean; allowConfigureFields?: boolean;
} }
export class AclAvailableAction { export class ACLAvailableAction {
constructor(public name: string, public options: AvailableActionOptions) {} constructor(public name: string, public options: AvailableActionOptions) {}
} }

View File

@ -9,22 +9,6 @@ export interface AvailableStrategyOptions {
resource?: '*'; resource?: '*';
} }
export function strategyValueMatched(strategy: StrategyValue, value: string) {
if (strategy === '*') {
return true;
}
if (lodash.isString(strategy) && strategy === value) {
return true;
}
if (lodash.isArray(strategy) && strategy.includes(value)) {
return true;
}
return false;
}
export const predicate = { export const predicate = {
own: { own: {
filter: { filter: {

View File

@ -53,7 +53,7 @@ export class ACLResource {
this.actions.set(name, context.params); this.actions.set(name, context.params);
} }
setActions(actions: { [key: string]: RoleActionParams }) { setActions(actions: ResourceActions) {
for (const actionName of Object.keys(actions)) { for (const actionName of Object.keys(actions)) {
this.setAction(actionName, actions[actionName]); this.setAction(actionName, actions[actionName]);
} }

View File

@ -11,7 +11,7 @@ export interface RoleActionParams {
[key: string]: any; [key: string]: any;
} }
interface ResourceActionsOptions { export interface ResourceActionsOptions {
[actionName: string]: RoleActionParams; [actionName: string]: RoleActionParams;
} }
@ -25,33 +25,16 @@ export class ACLRole {
return this.resources.get(name); return this.resources.get(name);
} }
setResource(name: string, resource: ACLResource) {
this.resources.set(name, resource);
}
public setStrategy(value: string | AvailableStrategyOptions) { public setStrategy(value: string | AvailableStrategyOptions) {
this.strategy = value; this.strategy = value;
} }
public grantResource(resourceName: string, options: ResourceActionsOptions) {
const resource = new ACLResource({
role: this,
name: resourceName,
});
for (const [actionName, actionParams] of Object.entries(options)) {
resource.setAction(actionName, actionParams);
}
this.resources.set(resourceName, resource);
}
public getResourceActionsParams(resourceName: string) { public getResourceActionsParams(resourceName: string) {
const resource = this.getResource(resourceName); const resource = this.getResource(resourceName);
return resource.getActions(); return resource.getActions();
} }
public revokeResource(resourceName) { public revokeResource(resourceName: string) {
for (const key of [...this.resources.keys()]) { for (const key of [...this.resources.keys()]) {
if (key === resourceName || key.includes(`${resourceName}.`)) { if (key === resourceName || key.includes(`${resourceName}.`)) {
this.resources.delete(key); this.resources.delete(key);

View File

@ -4,10 +4,10 @@ import EventEmitter from 'events';
import parse from 'json-templates'; import parse from 'json-templates';
import compose from 'koa-compose'; import compose from 'koa-compose';
import lodash from 'lodash'; import lodash from 'lodash';
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action'; import { ACLAvailableAction, AvailableActionOptions } from './acl-available-action';
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy'; import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
import { ACLRole, RoleActionParams } from './acl-role'; import { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role';
import { AllowManager } from './allow-manager'; import { AllowManager, ConditionFunc } from './allow-manager';
interface CanResult { interface CanResult {
role: string; role: string;
@ -19,10 +19,8 @@ interface CanResult {
export interface DefineOptions { export interface DefineOptions {
role: string; role: string;
allowConfigure?: boolean; allowConfigure?: boolean;
strategy?: string | Omit<AvailableStrategyOptions, 'acl'>; strategy?: string | AvailableStrategyOptions;
actions?: { actions?: ResourceActionsOptions;
[key: string]: RoleActionParams;
};
routes?: any; routes?: any;
} }
@ -44,7 +42,7 @@ interface CanArgs {
} }
export class ACL extends EventEmitter { export class ACL extends EventEmitter {
protected availableActions = new Map<string, AclAvailableAction>(); protected availableActions = new Map<string, ACLAvailableAction>();
protected availableStrategy = new Map<string, ACLAvailableStrategy>(); protected availableStrategy = new Map<string, ACLAvailableStrategy>();
protected middlewares: Toposort<any>; protected middlewares: Toposort<any>;
@ -131,7 +129,7 @@ export class ACL extends EventEmitter {
} }
setAvailableAction(name: string, options: AvailableActionOptions = {}) { setAvailableAction(name: string, options: AvailableActionOptions = {}) {
this.availableActions.set(name, new AclAvailableAction(name, options)); this.availableActions.set(name, new ACLAvailableAction(name, options));
if (options.aliases) { if (options.aliases) {
const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases]; const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases];
@ -150,7 +148,7 @@ export class ACL extends EventEmitter {
return this.availableActions; return this.availableActions;
} }
setAvailableStrategy(name: string, options: Omit<AvailableStrategyOptions, 'acl'>) { setAvailableStrategy(name: string, options: AvailableStrategyOptions) {
this.availableStrategy.set(name, new ACLAvailableStrategy(this, options)); this.availableStrategy.set(name, new ACLAvailableStrategy(this, options));
} }
@ -222,7 +220,7 @@ export class ACL extends EventEmitter {
this.middlewares.add(fn, options); this.middlewares.add(fn, options);
} }
allow(resourceName: string, actionNames: string[] | string, condition?: any) { allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
if (!Array.isArray(actionNames)) { if (!Array.isArray(actionNames)) {
actionNames = [actionNames]; actionNames = [actionNames];
} }

View File

@ -1,6 +1,6 @@
import { ACL } from './acl'; import { ACL } from './acl';
type ConditionFunc = (ctx: any) => Promise<boolean>; export type ConditionFunc = (ctx: any) => Promise<boolean>;
export class AllowManager { export class AllowManager {
protected skipActions = new Map<string, Map<string, string | ConditionFunc | true>>(); protected skipActions = new Map<string, Map<string, string | ConditionFunc | true>>();
@ -20,7 +20,7 @@ export class AllowManager {
const roleInstance = await ctx.db.getRepository('roles').findOne({ const roleInstance = await ctx.db.getRepository('roles').findOne({
filter: { filter: {
name: roleName name: roleName,
}, },
}); });

View File

@ -4,4 +4,3 @@ export * from './acl-available-strategy';
export * from './acl-resource'; export * from './acl-resource';
export * from './acl-role'; export * from './acl-role';
export * from './skip-middleware'; export * from './skip-middleware';

View File

@ -31,6 +31,23 @@ describe('has one repository', () => {
await db.sync(); await db.sync();
}); });
test('create', async () => {
const user = await User.repository.create({
values: { name: 'u1' },
});
const userProfileRepository = new HasOneRepository(User, 'profile', user['id']);
let profile = await userProfileRepository.find();
profile = await userProfileRepository.create({
values: {
avatar: 'avatar1',
},
});
console.log(profile.toJSON());
});
test('find', async () => { test('find', async () => {
const user = await User.repository.create({ const user = await User.repository.create({
values: { name: 'u1' }, values: { name: 'u1' },

View File

@ -20,7 +20,7 @@ export function getConfigByEnv() {
database: process.env.DB_DATABASE, database: process.env.DB_DATABASE,
host: process.env.DB_HOST, host: process.env.DB_HOST,
port: process.env.DB_PORT, port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT, dialect: process.env.DB_DIALECT || 'sqlite',
logging: process.env.DB_LOGGING === 'on' ? console.log : false, logging: process.env.DB_LOGGING === 'on' ? console.log : false,
storage: storage:
process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:' process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:'

View File

@ -53,7 +53,7 @@ export abstract class RelationRepository {
} }
@transaction() @transaction()
async create(options?: CreateOptions): Promise<any> { async create(options?: CreateOptions): Promise<Model> {
const createAccessor = this.accessors().create; const createAccessor = this.accessors().create;
const guard = UpdateGuard.fromOptions(this.targetModel, options); const guard = UpdateGuard.fromOptions(this.targetModel, options);

View File

@ -43,7 +43,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
}); });
} }
async find(options?: SingleRelationFindOption): Promise<Model<any>> { async find(options?: SingleRelationFindOption): Promise<Model<any> | null> {
const transaction = await this.getTransaction(options); const transaction = await this.getTransaction(options);
const findOptions = this.buildQueryOptions({ const findOptions = this.buildQueryOptions({
...options, ...options,