diff --git a/docs/menus.ts b/docs/menus.ts index 95c8c09de..d99dbe469 100644 --- a/docs/menus.ts +++ b/docs/menus.ts @@ -55,7 +55,7 @@ export default { '/development/guide/i18n', '/development/guide/migration', { - title: 'UI Schema Designer', + title: 'UI 设计器', type: 'subMenu', children: [ // '/development/guide/ui-schema-designer/index', @@ -72,7 +72,6 @@ export default { }, '/development/guide/ui-router', '/development/guide/settings-center', - '/development/guide/commands', ], }, { @@ -177,7 +176,10 @@ export default { '/api/database/collection', '/api/database/field', '/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', ], }, @@ -192,8 +194,17 @@ export default { ], }, { - title: '@nocobase/actions', - path: '/api/actions', + title: '@nocobase/acl', + 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', @@ -225,14 +236,14 @@ export default { }, ], }, - { - title: '@nocobase/acl', - path: '/api/acl', - }, { title: '@nocobase/cli', path: '/api/cli', }, + { + title: '@nocobase/actions', + path: '/api/actions', + }, { title: '@nocobase/sdk', path: '/api/sdk', diff --git a/docs/zh-CN/api/acl.md b/docs/zh-CN/api/acl.md deleted file mode 100644 index 8cd8bbcac..000000000 --- a/docs/zh-CN/api/acl.md +++ /dev/null @@ -1 +0,0 @@ -# ACL diff --git a/docs/zh-CN/api/acl/acl-available-action.md b/docs/zh-CN/api/acl/acl-available-action.md new file mode 100644 index 000000000..20a51393f --- /dev/null +++ b/docs/zh-CN/api/acl/acl-available-action.md @@ -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 是否允许配置字段 diff --git a/docs/zh-CN/api/acl/acl-available-strategy.md b/docs/zh-CN/api/acl/acl-available-strategy.md new file mode 100644 index 000000000..805edf60f --- /dev/null +++ b/docs/zh-CN/api/acl/acl-available-strategy.md @@ -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 定义,支持通配符 `*` diff --git a/docs/zh-CN/api/acl/acl-resource.md b/docs/zh-CN/api/acl/acl-resource.md new file mode 100644 index 000000000..c01283f3d --- /dev/null +++ b/docs/zh-CN/api/acl/acl-resource.md @@ -0,0 +1,57 @@ +# ACLResource + +ACLResource,ACL 系统中的资源类。在 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) diff --git a/docs/zh-CN/api/acl/acl-role.md b/docs/zh-CN/api/acl/acl-role.md new file mode 100644 index 000000000..f15af9155 --- /dev/null +++ b/docs/zh-CN/api/acl/acl-role.md @@ -0,0 +1,70 @@ +# ACL Role + +ACLRole,ACL 系统中的用户角色类。在 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 - 黑名单,黑名单中的字段不能被访问 diff --git a/docs/zh-CN/api/acl/acl.md b/docs/zh-CN/api/acl/acl.md new file mode 100644 index 000000000..dd3c6fa21 --- /dev/null +++ b/docs/zh-CN/api/acl/acl.md @@ -0,0 +1,214 @@ +# ACL + +ACL 为权限管理类,系统中的角色与资源都可以在 ACL 中进行注册。 + +## 成员变量 + +### `availableActions: Map` + +ACL 内的 `AclAvailableAction` 名称映射。 + +### `availableStrategy: Map` + +ACL 内的 [`ACLAvailableStrategy`](#ACLAvailableStrategy) 名称映射。 + +### `middlewares` + +ACL 鉴权中间件。 + +### `roles: Map` + +ACL 内的 `ACLRole` 名称映射。 + +### `actionAlias: Map` + +Action 别名映射。 + +### `configResources: Array` + +配置资源列表。 + +## 类方法 + +### `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; + }); + ``` diff --git a/docs/zh-CN/api/acl/allow-manager.md b/docs/zh-CN/api/acl/allow-manager.md new file mode 100644 index 000000000..ee9f9ad73 --- /dev/null +++ b/docs/zh-CN/api/acl/allow-manager.md @@ -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` + +获取已注册的开放条件 + +**参数** +* resourceName - 资源名称 +* actionName - 资源动作名 + +### `aclMiddleware()` + +中间件,注入于 `acl` 实例中,用于判断是否开放权限,若根据条件判断为开放权限,则在 `acl` middleware 中会跳过权限检查。 + diff --git a/docs/zh-CN/api/acl/index.md b/docs/zh-CN/api/acl/index.md new file mode 100644 index 000000000..fedf8476d --- /dev/null +++ b/docs/zh-CN/api/acl/index.md @@ -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` 函数,函数返回结果既为用户的鉴权结果。 diff --git a/docs/zh-CN/api/database/relation-repository.md b/docs/zh-CN/api/database/relation-repository.md deleted file mode 100644 index 32e9879c7..000000000 --- a/docs/zh-CN/api/database/relation-repository.md +++ /dev/null @@ -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()` diff --git a/docs/zh-CN/api/database/relation-repository/belongs-to-many-repository.md b/docs/zh-CN/api/database/relation-repository/belongs-to-many-repository.md new file mode 100644 index 000000000..c16d0e126 --- /dev/null +++ b/docs/zh-CN/api/database/relation-repository/belongs-to-many-repository.md @@ -0,0 +1,23 @@ +## BelongsToManyRepository + +### `count()` + +### `find()` + +### `findOne()` + +### `findAndCount()` + +### `create()` + +### `update()` + +### `destroy()` + +### `add()` + +### `remove()` + +### `set()` + +### `toggle()` diff --git a/docs/zh-CN/api/database/relation-repository/belongs-to-repository.md b/docs/zh-CN/api/database/relation-repository/belongs-to-repository.md new file mode 100644 index 000000000..f19d75935 --- /dev/null +++ b/docs/zh-CN/api/database/relation-repository/belongs-to-repository.md @@ -0,0 +1,3 @@ +## BelongsToRepository + +`BelongsToRepository` 是用于处理 `BelongsTo` 关系的 `Repository`,它提供了一些便捷的方法来处理 `BelongsTo` 关系。其接口与 [HasOneRepository](#has-one-repository) 一致。 diff --git a/docs/zh-CN/api/database/relation-repository/has-many-repository.md b/docs/zh-CN/api/database/relation-repository/has-many-repository.md new file mode 100644 index 000000000..d5dd9d0ae --- /dev/null +++ b/docs/zh-CN/api/database/relation-repository/has-many-repository.md @@ -0,0 +1,152 @@ + +# HasManyRepository + +`HasManyRepository` 是用于处理 `HasMany` 关系的 `Repository`。 + +## 类方法 + +### `find()` + +查找关联对象 + +**签名** + +* `async find(options?: FindOptions): Promise` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `options` | `FindOptions` | - | 参见 repository.find | + +### `findOne()` + +查找关联对象,仅返回一条记录 + +**签名** + +* `async findOne(options?: FindOneOptions): Promise` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `options` | `CreateOptions` | - | 参见 repository.create | + + +### `update()` + +更新符合条件的关联对象 + +**签名** + +* `async update(options?: UpdateOptions): Promise` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `options` | `UpdateOptions` | - | 参见 repository.update | + + +### `destroy()` + +删除符合条件的关联对象 + +**签名** + +* `async destroy(options?: TK | DestroyOptions): Promise` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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) | diff --git a/docs/zh-CN/api/database/relation-repository/has-one-repository.md b/docs/zh-CN/api/database/relation-repository/has-one-repository.md new file mode 100644 index 000000000..5ecc683df --- /dev/null +++ b/docs/zh-CN/api/database/relation-repository/has-one-repository.md @@ -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` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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 | 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` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `options` | `UpdateOptions` | - | 参见 repository.update | + +**示例** + +```typescript +const profile = await UserProfileRepository.update({ + values: { avatar: 'avatar2' }, +}); + +profile.get('avatar'); // 'avatar2' +``` + +### `remove()` + +移除关联对象,仅解除关联关系,不删除关联对象 + +**签名** + +* `async remove(options?: Transactionable): Promise` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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` + + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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` + +**参数** + +| 参数名 | 类型 | 默认值 | 描述 | +| --- | --- | --- | --- | +| `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 +``` diff --git a/docs/zh-CN/api/database/relation-repository/index.md b/docs/zh-CN/api/database/relation-repository/index.md new file mode 100644 index 000000000..27140968d --- /dev/null +++ b/docs/zh-CN/api/database/relation-repository/index.md @@ -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 值 diff --git a/docs/zh-CN/development/guide/middleware.md b/docs/zh-CN/development/guide/middleware.md index e3bc66b71..fbb4ecc9f 100644 --- a/docs/zh-CN/development/guide/middleware.md +++ b/docs/zh-CN/development/guide/middleware.md @@ -1,4 +1,4 @@ -# Middleware +# 中间件 ## 添加方法 diff --git a/docs/zh-CN/development/guide/settings-center.md b/docs/zh-CN/development/guide/settings-center.md index 94b1d0431..37286175c 100644 --- a/docs/zh-CN/development/guide/settings-center.md +++ b/docs/zh-CN/development/guide/settings-center.md @@ -1,4 +1,4 @@ -# Settings Center +# 配置中心 diff --git a/docs/zh-CN/development/guide/ui-router.md b/docs/zh-CN/development/guide/ui-router.md index 5894440f7..5a620942b 100644 --- a/docs/zh-CN/development/guide/ui-router.md +++ b/docs/zh-CN/development/guide/ui-router.md @@ -1,4 +1,4 @@ -# UI Router +# UI 路由 NocoBase Client 的 Router 基于 [React Router](https://v5.reactrouter.com/web/guides/quick-start),可以通过 `` 来配置 ui routes,例子如下: diff --git a/packages/core/acl/src/__tests__/acl.test.ts b/packages/core/acl/src/__tests__/acl.test.ts index 895ded280..579e60919 100644 --- a/packages/core/acl/src/__tests__/acl.test.ts +++ b/packages/core/acl/src/__tests__/acl.test.ts @@ -38,6 +38,7 @@ describe('acl', () => { }, }); }); + it('should define role with predicate', () => { acl.setAvailableAction('edit', { type: 'old-data', diff --git a/packages/core/acl/src/acl-available-action.ts b/packages/core/acl/src/acl-available-action.ts index b64eca973..3f0e9ac3e 100644 --- a/packages/core/acl/src/acl-available-action.ts +++ b/packages/core/acl/src/acl-available-action.ts @@ -12,6 +12,6 @@ export interface AvailableActionOptions { allowConfigureFields?: boolean; } -export class AclAvailableAction { +export class ACLAvailableAction { constructor(public name: string, public options: AvailableActionOptions) {} } diff --git a/packages/core/acl/src/acl-available-strategy.ts b/packages/core/acl/src/acl-available-strategy.ts index aaf3f16a2..5e664d9ad 100644 --- a/packages/core/acl/src/acl-available-strategy.ts +++ b/packages/core/acl/src/acl-available-strategy.ts @@ -9,22 +9,6 @@ export interface AvailableStrategyOptions { 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 = { own: { filter: { diff --git a/packages/core/acl/src/acl-resource.ts b/packages/core/acl/src/acl-resource.ts index 60fe97551..f3592deeb 100644 --- a/packages/core/acl/src/acl-resource.ts +++ b/packages/core/acl/src/acl-resource.ts @@ -53,7 +53,7 @@ export class ACLResource { this.actions.set(name, context.params); } - setActions(actions: { [key: string]: RoleActionParams }) { + setActions(actions: ResourceActions) { for (const actionName of Object.keys(actions)) { this.setAction(actionName, actions[actionName]); } diff --git a/packages/core/acl/src/acl-role.ts b/packages/core/acl/src/acl-role.ts index 46f377851..0b1ec76de 100644 --- a/packages/core/acl/src/acl-role.ts +++ b/packages/core/acl/src/acl-role.ts @@ -11,7 +11,7 @@ export interface RoleActionParams { [key: string]: any; } -interface ResourceActionsOptions { +export interface ResourceActionsOptions { [actionName: string]: RoleActionParams; } @@ -25,33 +25,16 @@ export class ACLRole { return this.resources.get(name); } - setResource(name: string, resource: ACLResource) { - this.resources.set(name, resource); - } - public setStrategy(value: string | AvailableStrategyOptions) { 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) { const resource = this.getResource(resourceName); return resource.getActions(); } - public revokeResource(resourceName) { + public revokeResource(resourceName: string) { for (const key of [...this.resources.keys()]) { if (key === resourceName || key.includes(`${resourceName}.`)) { this.resources.delete(key); diff --git a/packages/core/acl/src/acl.ts b/packages/core/acl/src/acl.ts index 1117eb9f5..dcc14e932 100644 --- a/packages/core/acl/src/acl.ts +++ b/packages/core/acl/src/acl.ts @@ -4,10 +4,10 @@ import EventEmitter from 'events'; import parse from 'json-templates'; import compose from 'koa-compose'; 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 { ACLRole, RoleActionParams } from './acl-role'; -import { AllowManager } from './allow-manager'; +import { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role'; +import { AllowManager, ConditionFunc } from './allow-manager'; interface CanResult { role: string; @@ -19,10 +19,8 @@ interface CanResult { export interface DefineOptions { role: string; allowConfigure?: boolean; - strategy?: string | Omit; - actions?: { - [key: string]: RoleActionParams; - }; + strategy?: string | AvailableStrategyOptions; + actions?: ResourceActionsOptions; routes?: any; } @@ -44,7 +42,7 @@ interface CanArgs { } export class ACL extends EventEmitter { - protected availableActions = new Map(); + protected availableActions = new Map(); protected availableStrategy = new Map(); protected middlewares: Toposort; @@ -131,7 +129,7 @@ export class ACL extends EventEmitter { } setAvailableAction(name: string, options: AvailableActionOptions = {}) { - this.availableActions.set(name, new AclAvailableAction(name, options)); + this.availableActions.set(name, new ACLAvailableAction(name, options)); if (options.aliases) { const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases]; @@ -150,7 +148,7 @@ export class ACL extends EventEmitter { return this.availableActions; } - setAvailableStrategy(name: string, options: Omit) { + setAvailableStrategy(name: string, options: AvailableStrategyOptions) { this.availableStrategy.set(name, new ACLAvailableStrategy(this, options)); } @@ -222,7 +220,7 @@ export class ACL extends EventEmitter { 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)) { actionNames = [actionNames]; } diff --git a/packages/core/acl/src/allow-manager.ts b/packages/core/acl/src/allow-manager.ts index 84f80e8e4..0c39cf459 100644 --- a/packages/core/acl/src/allow-manager.ts +++ b/packages/core/acl/src/allow-manager.ts @@ -1,6 +1,6 @@ import { ACL } from './acl'; -type ConditionFunc = (ctx: any) => Promise; +export type ConditionFunc = (ctx: any) => Promise; export class AllowManager { protected skipActions = new Map>(); @@ -20,7 +20,7 @@ export class AllowManager { const roleInstance = await ctx.db.getRepository('roles').findOne({ filter: { - name: roleName + name: roleName, }, }); diff --git a/packages/core/acl/src/index.ts b/packages/core/acl/src/index.ts index 7ccff4ff2..391b93064 100644 --- a/packages/core/acl/src/index.ts +++ b/packages/core/acl/src/index.ts @@ -4,4 +4,3 @@ export * from './acl-available-strategy'; export * from './acl-resource'; export * from './acl-role'; export * from './skip-middleware'; - diff --git a/packages/core/database/src/__tests__/relation-repository/hasone-repository.test.ts b/packages/core/database/src/__tests__/relation-repository/hasone-repository.test.ts index a006f14bb..8c710981f 100644 --- a/packages/core/database/src/__tests__/relation-repository/hasone-repository.test.ts +++ b/packages/core/database/src/__tests__/relation-repository/hasone-repository.test.ts @@ -31,6 +31,23 @@ describe('has one repository', () => { 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 () => { const user = await User.repository.create({ values: { name: 'u1' }, diff --git a/packages/core/database/src/mock-database.ts b/packages/core/database/src/mock-database.ts index 58a45be0d..6a993a97d 100644 --- a/packages/core/database/src/mock-database.ts +++ b/packages/core/database/src/mock-database.ts @@ -20,7 +20,7 @@ export function getConfigByEnv() { database: process.env.DB_DATABASE, host: process.env.DB_HOST, 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, storage: process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:' diff --git a/packages/core/database/src/relation-repository/relation-repository.ts b/packages/core/database/src/relation-repository/relation-repository.ts index fbdb28b86..a092e4f23 100644 --- a/packages/core/database/src/relation-repository/relation-repository.ts +++ b/packages/core/database/src/relation-repository/relation-repository.ts @@ -53,7 +53,7 @@ export abstract class RelationRepository { } @transaction() - async create(options?: CreateOptions): Promise { + async create(options?: CreateOptions): Promise { const createAccessor = this.accessors().create; const guard = UpdateGuard.fromOptions(this.targetModel, options); diff --git a/packages/core/database/src/relation-repository/single-relation-repository.ts b/packages/core/database/src/relation-repository/single-relation-repository.ts index 899f7d5e0..d403ab4df 100644 --- a/packages/core/database/src/relation-repository/single-relation-repository.ts +++ b/packages/core/database/src/relation-repository/single-relation-repository.ts @@ -43,7 +43,7 @@ export abstract class SingleRelationRepository extends RelationRepository { }); } - async find(options?: SingleRelationFindOption): Promise> { + async find(options?: SingleRelationFindOption): Promise | null> { const transaction = await this.getTransaction(options); const findOptions = this.buildQueryOptions({ ...options,