fix: delete root docs (#3145)

* fix: delete root docs

* docs: update useSchemaOptionsContext()

* chore: change ci

* chore: upgrade dumi theme

* fix: upgrade tsx
This commit is contained in:
jack zhang 2023-12-06 16:20:31 +08:00 committed by GitHub
parent 269467ebb0
commit 8e7cb832cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
441 changed files with 188 additions and 33610 deletions

View File

@ -1,11 +0,0 @@
import React from "react";
const LangSwitch = () => {
const { hostname } = window.location
if (hostname === 'localhost') return null;
const en = window.location.href.replace(hostname, 'docs.nocobase.com')
const cn = window.location.href.replace(hostname, 'docs-cn.nocobase.com')
return <span><a href={en}>EN</a> | <a href={cn}></a></span>
}
export default LangSwitch;

View File

@ -1,23 +0,0 @@
import React, { FC, useRef, useEffect, Suspense } from 'react';
import { Root, createRoot } from 'react-dom/client';
import { IPreviewerProps } from 'dumi';
import DefaultPreviewer from 'dumi/theme-default/builtins/Previewer';
const Previewer: FC<IPreviewerProps> = ({ children, ...props }) => {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
let root: Root
if (ref.current) {
root = createRoot(ref.current)
root.render(<Suspense fallback={<div>loading...</div>}>{children}</Suspense>)
}
return () => {
if (root) {
root.unmount()
}
}
}, []);
return <DefaultPreviewer {...props}><div ref={ref} /></DefaultPreviewer>;
};
export default Previewer;

View File

@ -1,219 +0,0 @@
import DumiPreviewerActions from 'dumi/theme-default/slots/PreviewerActions';
import React, { useRef, useEffect, useState } from 'react';
import { Spin } from 'antd'
import { IPreviewerProps } from 'dumi';
const indexHtml = `<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
`
const mainTsx = `
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
`
const packageJson = `
{
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
},
"devDependencies": {
"flat": "^5.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.0.3",
"less": "^4.2.0",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}
`
const tsConfigJson = `
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"composite": true,
"strict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowSyntheticDefaultImports": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src",
"vite.config.ts"
]
}
`
const viteConfigTs = `
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
`
const sandboxTask = `
{
"setupTasks": [
{
"name": "Install Dependencies",
"command": "yarn install"
}
],
"tasks": {
"dev": {
"name": "dev",
"command": "yarn dev",
"runAtStart": true,
"preview": {
"port": 5173
}
},
"build": {
"name": "build",
"command": "yarn build",
"runAtStart": false
},
"preview": {
"name": "preview",
"command": "yarn preview",
"runAtStart": false
}
}
}
`
function getCSBData(opts: IPreviewerProps, ext: string) {
const files: Record<
string,
{
content: string;
isBinary: boolean;
}
> = {};
const deps: Record<string, string> = {};
const entryFileName = `index${ext}`;
Object.entries(opts.asset.dependencies).forEach(([name, { type, value }]) => {
if (type === 'NPM') {
// generate dependencies
deps[name] = value;
} else {
// append other imported local files
files[name === entryFileName ? `src/App${ext}` : name] = {
content: value,
isBinary: false,
};
}
});
// append package.json
let pkg = JSON.parse(packageJson)
try {
for (let key in deps) {
if (!pkg['devDependencies'][key]) {
pkg.dependencies[key] = deps[key]
}
}
} catch (e) {
console.log(e)
}
files['package.json'] = {
content: JSON.stringify(
{
name: opts.title,
...pkg,
},
null,
2,
),
isBinary: false,
};
files['index.html'] = { content: indexHtml, isBinary: false };
files['src/main.tsx'] = { content: mainTsx, isBinary: false };
files['package.json'] = { content: JSON.stringify(pkg, null, 2), isBinary: false };
files['.codesandbox/task.json'] = { content: sandboxTask, isBinary: false };
files['tsconfig.json'] = { content: tsConfigJson, isBinary: false };
files['vite.config.ts'] = { content: viteConfigTs, isBinary: false };
return { files };
}
export function openCodeSandbox(opts: IPreviewerProps) {
const isTSX = Boolean(opts.asset.dependencies?.['index.tsx']);
const ext = isTSX ? '.tsx' : '.jsx';
return fetch("https://codesandbox.io/api/v1/sandboxes/define?json=1", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify(getCSBData(opts, ext))
})
.then(x => x.json())
.then(data => {
window.open(`https://codesandbox.io/p/sandbox/${data.sandbox_id}?file=/src/App${ext}`);
});
}
const PreviewerActions: typeof DumiPreviewerActions = (props) => {
const div = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (div.current) {
const element = div.current.querySelector('.dumi-default-previewer-action-btn');
element?.addEventListener('click', (e) => {
e.stopImmediatePropagation();
setLoading(true);
openCodeSandbox(props).finally(() => {
setLoading(false);
});
})
}
}, [div])
return <Spin spinning={loading}><div ref={div}><DumiPreviewerActions {...props} disabledActions={['STACKBLITZ']} /></div></Spin>
};
export default PreviewerActions;

View File

@ -1,61 +0,0 @@
import { getUmiConfig } from '@nocobase/devtools/umiConfig';
import { defineConfig } from 'dumi';
import { defineThemeConfig } from 'dumi-theme-nocobase';
import { nav, sidebar } from './docs/config';
const umiConfig = getUmiConfig();
const lang = process.env.DOC_LANG || 'en-US';
console.log('process.env.DOC_LANG', process.env.DOC_LANG);
// 设置多语言的 title
function setTitle(menuChildren) {
if (!menuChildren) return;
menuChildren.forEach((item) => {
if (typeof item === 'object') {
item.title = item[`title.${lang}`] || item.title;
if (item.children) {
setTitle(item.children);
}
}
});
}
if (lang !== 'en-US') {
Object.values(sidebar).forEach(setTitle);
}
export default defineConfig({
hash: true,
alias: {
...umiConfig.alias,
},
ssr: {
},
exportStatic: {
ignorePreRenderError: true
},
cacheDirectoryPath: `node_modules/.docs-${lang}-cache`,
outputPath: `./docs/dist/${lang}`,
resolve: {
docDirs: [`./docs/${lang}`]
},
locales: [
{ id: 'en-US', name: 'English' },
{ id: 'zh-CN', name: '中文' },
],
themeConfig: defineThemeConfig({
title: 'NocoBase',
logo: 'https://www.nocobase.com/images/logo.png',
nav: nav.map((item) => ({ ...item, title: (item[`title.${lang}`] || item.title) })),
sidebarEnhance: sidebar as any,
github: 'https://github.com/nocobase/nocobase',
footer: 'nocobase | Copyright © 2022',
localesEnhance: [
{ id: 'zh-CN', switchPrefix: '中', hostname: 'docs-cn.nocobase.com' },
{ id: 'en-US', switchPrefix: 'en', hostname: 'docs.nocobase.com' }
],
}),
// mfsu: true, // 报错
});

View File

@ -5,12 +5,14 @@ on:
- 'main' - 'main'
paths: paths:
- 'packages/core/client/**' - 'packages/core/client/**'
- 'packages/core/client/docs/**'
- '.github/workflows/deploy-client-docs.yml' - '.github/workflows/deploy-client-docs.yml'
pull_request: pull_request:
branches: branches:
- '**' - '**'
paths: paths:
- 'packages/core/client/**' - 'packages/core/client/**'
- 'packages/core/client/docs/**'
- '.github/workflows/deploy-client-docs.yml' - '.github/workflows/deploy-client-docs.yml'
jobs: jobs:

View File

@ -1,296 +0,0 @@
const nav = [
{
title: 'Welcome',
'title.zh-CN': '欢迎',
link: '/welcome/introduction',
},
{
title: 'User manual',
'title.zh-CN': '使用手册',
link: '/manual/quick-start/the-first-app',
},
{
title: 'Plugin Development',
'title.zh-CN': '插件开发',
link: '/development',
},
{
title: 'API reference',
'title.zh-CN': 'API 参考',
link: '/api',
},
{
title: 'Schema components',
'title.zh-CN': 'Schema 组件库',
link: '/components',
},
];
const sidebar = {
'/welcome': [
{
title: 'Welcome',
'title.zh-CN': '欢迎',
'title.tr-TR': 'Hoşgeldiniz',
type: 'group',
children: [
'/welcome/introduction',
'/welcome/introduction/features',
'/welcome/introduction/why',
// '/welcome/introduction/learning-guide',
],
},
{
title: 'Getting started',
'title.zh-CN': '快速开始',
'title.tr-TR': 'Başlangıç',
type: 'group',
children: [
{
title: 'Installation',
'title.zh-CN': '安装',
'title.TR-TR': 'Kurulum',
children: [
'/welcome/getting-started/installation',
'/welcome/getting-started/installation/docker-compose',
'/welcome/getting-started/installation/create-nocobase-app',
'/welcome/getting-started/installation/git-clone',
],
},
{
title: 'Upgrading',
'title.zh-CN': '升级',
'title.TR-TR': 'Güncelleme',
children: [
'/welcome/getting-started/upgrading',
'/welcome/getting-started/upgrading/docker-compose',
'/welcome/getting-started/upgrading/create-nocobase-app',
'/welcome/getting-started/upgrading/git-clone',
],
},
],
},
{
title: 'Releases',
'title.zh-CN': '产品发布',
'title.TR-TR': 'Sürüm',
type: 'group',
children: [
{
type: 'item',
title: 'Changelog',
'title.zh-CN': '更新日志',
link: 'https://github.com/nocobase/nocobase/blob/main/CHANGELOG.md',
},
// '/welcome/release/index',
// '/welcome/release/v08-changelog',
'/welcome/release/v14-changelog',
'/welcome/release/v13-changelog',
'/welcome/release/v12-changelog',
'/welcome/release/v11-changelog',
'/welcome/release/v10-changelog',
],
},
{
title: 'Community',
'title.zh-CN': '社区',
'title.TR-TR': 'Topluluk',
type: 'group',
children: [
'/welcome/community/contributing',
// '/welcome/community/faq',
'/welcome/community/translations',
'/welcome/community/thanks',
],
},
],
'/manual': [
{
title: 'Quick Start',
'title.zh-CN': '快速上手',
'title.TR-TR': 'Hızlı Başlangıç',
type: 'group',
children: [
'/manual/quick-start/the-first-app',
'/manual/quick-start/functional-zoning',
'/manual/quick-start/ui-editor-mode',
],
},
{
title: 'Core Concepts',
'title.zh-CN': '核心概念',
'title.TR-TR': 'Temel Kavramlar',
type: 'group',
children: [
'/manual/core-concepts/collections',
'/manual/core-concepts/blocks',
'/manual/core-concepts/actions',
'/manual/core-concepts/menus',
'/manual/core-concepts/containers',
],
},
],
'/development': [
{
title: 'Getting started',
'title.zh-CN': '快速开始',
'title.TR-TR': 'Başlarken',
type: 'group',
children: [
'/development',
'/development/your-fisrt-plugin',
'/development/app-ds',
'/development/plugin-ds',
'/development/life-cycle',
// '/development/learning-guide',
],
},
{
title: 'Server',
'title.zh-CN': '服务端',
'title.TR-TR': 'Sunucu',
type: 'group',
children: [
'/development/server',
{
title: 'Collections & Fields',
'title.zh-CN': '数据表和字段',
'title.TR-TR': 'Koleksiyonlar & Alanlar',
children: [
'/development/server/collections',
'/development/server/collections/options',
'/development/server/collections/configure',
'/development/server/collections/association-fields',
'/development/server/collections/field-extension',
'/development/server/collections/collection-template',
],
},
// '/development/server/collections-fields',
'/development/server/resources-actions',
'/development/server/middleware',
'/development/server/commands',
'/development/server/events',
'/development/server/i18n',
'/development/server/migration',
'/development/server/test',
],
},
{
title: 'Client',
'title.zh-CN': '客户端',
'title.TR-TR': 'Ziyaretçi(Client)',
type: 'group',
children: [
'/development/client',
{
title: 'UI designer',
'title.zh-CN': 'UI 设计器',
'title.TR-TR': 'Kullanıcı Arayüz Tasarımcısı',
children: [
// '/development/client/ui-schema-designer',
'/development/client/ui-schema-designer/what-is-ui-schema',
'/development/client/ui-schema-designer/extending-schema-components',
// '/development/client/ui-schema-designer/insert-adjacent',
'/development/client/ui-schema-designer/designable',
'/development/client/ui-schema-designer/component-library',
// '/development/client/ui-schema-designer/collection-manager',
// '/development/client/ui-schema-designer/acl',
'/development/client/ui-schema-designer/x-designer',
'/development/client/ui-schema-designer/x-initializer',
],
},
'/development/client/ui-router',
'/development/client/plugin-settings',
'/development/client/i18n',
'/development/client/test',
],
},
],
'/api': [
'/api',
'/api/env',
{
title: 'HTTP API',
type: 'subMenu',
children: ['/api/http', '/api/http/rest-api'],
},
{
title: '@nocobase/server',
type: 'subMenu',
children: [
'/api/server/application',
// '/api/server/plugin-manager',
'/api/server/plugin',
],
},
{
title: '@nocobase/database',
type: 'subMenu',
children: [
'/api/database',
'/api/database/collection',
'/api/database/field',
'/api/database/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',
],
},
{
title: '@nocobase/resourcer',
type: 'subMenu',
children: ['/api/resourcer', '/api/resourcer/resource', '/api/resourcer/action', '/api/resourcer/middleware'],
},
{
title: '@nocobase/acl',
type: 'subMenu',
children: ['/api/acl/acl', '/api/acl/acl-role', '/api/acl/acl-resource'],
},
{
title: '@nocobase/client',
type: 'subMenu',
children: [
// '/api/client',
'/api/client/application',
'/api/client/router',
{
title: 'SchemaDesigner',
'title.zh-CN': 'SchemaDesigner',
'title.TR-TR': 'Şema Tasarımcısı',
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',
'title.TR-TR': 'Eklentiler',
children: [
// '/api/client/extensions/schema-component',
'/api/client/extensions/collection-manager',
'/api/client/extensions/block-provider',
'/api/client/extensions/acl',
],
},
],
},
{
title: '@nocobase/cli',
link: '/api/cli',
},
{
title: '@nocobase/actions',
link: '/api/actions',
},
{
title: '@nocobase/sdk',
link: '/api/sdk',
},
],
};
export { nav, sidebar };

View File

@ -1,59 +0,0 @@
# ACLResource
ACLResource is the resource class in ACL system. In ACL systems, the corresponding resource is created automatically when granting permission to user.
## Class Methods
### `constructor()`
Constructor.
**Signature**
* `constructor(options: AclResourceOptions)`
**Type**
```typescript
type ResourceActions = { [key: string]: RoleActionParams };
interface AclResourceOptions {
name: string; // Name of the resource
role: ACLRole; // Role to which the resource belongs
actions?: ResourceActions;
}
```
**Detailed Information**
Refer to [`aclRole.grantAction`](./acl-role.md#grantaction) for details about `RoleActionParams`.
### `getActions()`
Get all actions of the resource, the return is `ResourceActions` object.
### `getAction()`
Get the parameter configuration of the action by name, the return is `RoleActionParams` object.
**Detailed Information**
Refer to [`aclRole.grantAction`](./acl-role.md#grantaction) for
`RoleActionParams`.
### `setAction()`
Set the parameter configuration of an action inside the resource, the return is `RoleActionParams` object.
**Signature**
* `setAction(name: string, params: RoleActionParams)`
**Detailed Information**
* name - Name of the action to set
* Refer to [`aclRole.grantAction`](./acl-role.md#grantaction) for details about `RoleActionParams`.
### `setActions()`
**Signature**
* `setActions(actions: ResourceActions)`
A shortcut for calling `setAction` in batches.

View File

@ -1,87 +0,0 @@
# ACLRole
ACLRole is the user role class in ACL system. In ACL systems, roles are usually defined by `acl.define`.
## Class Methods
### `constructor()`
Constructor.
**Signature**
* `constructor(public acl: ACL, public name: string)`
**Detailed Information**
* acl - ACL instance
* name - Name of the role
### `grantAction()`
Grant the action permission to the role.
**Signature**
* `grantAction(path: string, options?: RoleActionParams)`
**Type**
```typescript
interface RoleActionParams {
fields?: string[];
filter?: any;
own?: boolean;
whitelist?: string[];
blacklist?: string[];
[key: string]: any;
}
```
**Detailed Information**
* path - Action path of the resource, such as `posts:edit`, which means the `edit` action of the `posts` resource. Use colon `:` to separate the name of resource and action.
When RoleActionParams is to grant permission, the corresponding action can be configured with parameters to achieve finer-grained permission control.
* fields - Accessible fields
```typescript
acl.define({
role: 'admin',
actions: {
'posts:view': {
// admin user can request posts:view action, but limited to the configured fields
fields: ["id", "title", "content"],
},
},
});
```
* filter - Permission resource filtering configuration
```typescript
acl.define({
role: 'admin',
actions: {
'posts:view': {
// admin user can request posts:view action, but the listed results is filtered by conditions in the filter
filter: {
createdById: '{{ ctx.state.currentUser.id }}', // Template syntax is supported to take the value in ctx, and will be replaced when checking permissions
},
},
},
});
```
* own - Whether to access only your own data
```typescript
const actionsWithOwn = {
'posts:view': {
"own": true //
}
}
// Equivalent to
const actionsWithFilter = {
'posts:view': {
"filter": {
"createdById": "{{ ctx.state.currentUser.id }}"
}
}
}
```
* whitelist - Whitelist, only the fields in whitelist can be accessed
* blacklist - Blacklist, fields in blacklist cannot be accessed

View File

@ -1,259 +0,0 @@
# ACL
## Overview
ACL is the permission control module in NocoBase. After registering roles and resources in ACL and configuring corresponding permissions, you can authenticate permissions for roles.
### Basic Usage
```javascript
const { ACL } = require('@nocobase/acl');
const acl = new ACL();
// Define a role named member
const memberRole = acl.define({
role: 'member',
});
// Grant the role of member list permission of the posts resource
memberRole.grantAction('posts:list');
acl.can('member', 'posts:list'); // true
acl.can('member', 'posts:edit'); // null
```
### Concepts
* Role (`ACLRole`): Object that needs permission authentication.
* Resource (`ACLResource`)In NocoBase ACL, a resource usually corresponds to a database table; it is conceptually analogous to the Resource in Restful API.
* Action: Actions to be taken on resources, such as `create`, `read`, `update`, `delete`, etc.
* Strategy (`ACLAvailableStrategy`): Normally each role has its own permission strategy, which defines the default permissions of the role.
* Grant Action: Call the `grantAction` function in `ACLRole` instance to grant access to `Action` for the role.
* Authentication: Call the `can` function in `ACL` instance, and return the authentication result of the user.
## Class Methods
### `constructor()`
To create a `ACL` instance.
```typescript
import { ACL } from '@nocobase/database';
const acl = new ACL();
```
### `define()`
Define a ACL role.
**Signature**
* `define(options: DefineOptions): ACLRole`
**Type**
```typescript
interface DefineOptions {
role: string;
allowConfigure?: boolean;
strategy?: string | AvailableStrategyOptions;
actions?: ResourceActionsOptions;
routes?: any;
}
```
**Detailed Information**
* `role` - Name of the role
```typescript
// Define a role named admin
acl.define({
role: 'admin',
});
```
* `allowConfigure` - Whether to allow permission configuration
* `strategy` - Permission strategy of the role
* It can be a name of strategy in `string`, means to use a strategy that is already defined.
* Or `AvailableStrategyOptions` means to define a new strategy for this role, refer to [`setAvailableActions()`](#setavailableactions).
* `actions` - Pass in the `actions` objects accessible to the role when defining the role, then call `aclRole.grantAction` in turn to grant resource permissions. Refer to [`aclRole.grantAction`](./acl-role.md#grantaction) for details
```typescript
acl.define({
role: 'admin',
actions: {
'posts:edit': {}
},
});
// Equivalent to
const role = acl.define({
role: 'admin',
});
role.grantAction('posts:edit', {});
```
### `getRole()`
Get registered role objects by role name.
**Signature**
* `getRole(name: string): ACLRole`
### `removeRole()`
Remove role by role name.
**Signature**
* `removeRole(name: string)`
### `can()`
Authentication function.
**Signature**
* `can(options: CanArgs): CanResult | null`
**Type**
```typescript
interface CanArgs {
role: string; // Name of the role
resource: string; // Name of the resource
action: string; // Name of the action
}
interface CanResult {
role: string; // Name of the role
resource: string; // Name of the resource
action: string; // Name of the action
params?: any; // Parameters passed in when registering the permission
}
```
**Detailed Information**
The `can` method first checks if the role has the corresponding `Action` permission registered; if not, it checks if the `strategy` and the role matches. It means that the role has no permissions if it returns `null`; else it returns the `CanResult` object, which means that the role has permissions.
**Example**
```typescript
// Define role and register permissions
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()`
**Signature**
* `use(fn: any)`
Add middleware function into middlewares.
### `middleware()`
Return a middleware function to be used in `@nocobase/server`. After using this `middleware`, `@nocobase/server` will perform permission authentication before each request is processed.
### `allow()`
Set the resource as publicly accessible.
**Signature**
* `allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc)`
**Type**
```typescript
type ConditionFunc = (ctx: any) => Promise<boolean> | boolean;
```
**Detailed Information**
* resourceName - Name of the resource
* actionNames - Name of the resource action
* condition? - Configuration of the validity condition
* Pass in a `string` to use a condition that is already defined; Use the `acl.allowManager.registerCondition` method to register a condition.
```typescript
acl.allowManager.registerAllowCondition('superUser', async () => {
return ctx.state.user?.id === 1;
});
// Open permissions of the users:list with validity condition superUser
acl.allow('users', 'list', 'superUser');
```
* Pass in ConditionFunc, which can take the `ctx` parameter; return `boolean` that indicate whether it is in effect.
```typescript
// user:list accessible to user with ID of 1
acl.allow('users', 'list', (ctx) => {
return ctx.state.user?.id === 1;
});
```
**Example**
```typescript
// Register users:login to be publicly accssible
acl.allow('users', 'login');
```
### `setAvailableActions()`
**Signature**
* `setAvailableStrategy(name: string, options: AvailableStrategyOptions)`
Register an available permission strategy.
**Type**
```typescript
interface AvailableStrategyOptions {
displayName?: string;
actions?: false | string | string[];
allowConfigure?: boolean;
resource?: '*';
}
```
**Detailed Information**
* displayName - Name of the strategy
* allowConfigure - Whether this strategy has permission of **resource configuration**; if set to `true`, the permission that requests to register as `configResources` resource in `ACL` will return pass
* actions - List of actions in the strategy, wildcard `*` is supported
* resource - Definition of resource in the strategy, wildcard `*` is supported

View File

@ -1,357 +0,0 @@
# Built-in Common Resource Actions
## Overview
NocoBase has built-in operation methods for commonly used actions of data resources, such as CRUD, and automatically maps related actions through data table resources.
```javascript
import { Application } from "@nocobase/server";
const app = new Application({
database: {
dialect: 'sqlite',
storage: './db.sqlite',
},
registerActions: true // Register built-in resource actions, true by default
});
```
Built-in actions are registered to the `resourcer` instance in `application`. Generally, built-in actions are not called directly unless you need to extend the default action, then you can call the default method within a custom action method.
## Resource Actions
### `list()`
Get a list of data. The URL for the corresponding resource action is `GET /api/<resource>:list`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | Filtering parameter |
| `fields` | `string[]` | - | Fields to get |
| `except` | `string[]` | - | Fields to exclude |
| `appends` | `string[]` | - | Association fields to append |
| `sort` | `string[]` | - | Sorting parameter |
| `page` | `number` | 1 | Page break |
| `pageSize` | `number` | 20 | Size per page |
**Example**
When there is a need to provide an interface for querying a list of data that is not output in JSON format by default, it can be extended based on the built-in default method:
```ts
import actions from '@nocobase/actions';
app.actions({
async ['books:list'](ctx, next) {
ctx.action.mergeParams({
except: ['content']
});
await actions.list(ctx, async () => {
const { rows } = ctx.body;
// transform JSON to CSV output
ctx.body = rows.map(row => Object.keys(row).map(key => row[key]).join(',')).join('\n');
ctx.type = 'text/csv';
await next();
});
}
});
```
Example of a request that will get a file in CSV format:
```shell
curl -X GET http://localhost:13000/api/books:list
```
### `get()`
Get a single piece of data. The URL for the corresponding resource action is `GET /api/<resource>:get`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `filterByTk` | `number \| string` | - | Filtering primary key |
| `filter` | `Filter` | - | Filtering parameter |
| `fields` | `string[]` | - | Fields to get |
| `except` | `string[]` | - | Fields to exclude |
| `appends` | `string[]` | - | Association fields to append |
| `sort` | `string[]` | - | Sorting parameter |
| `page` | `number` | 1 | Page break |
| `pageSize` | `number` | 20 | Size per page |
**Example**
Extend the build-in file management plugin of NocoBase to return file stream when the client requests to download a file with the resource identifier:
```ts
import path from 'path';
import actions from '@nocobase/actions';
import { STORAGE_TYPE_LOCAL } from '@nocobase/plugin-file-manager';
app.actions({
async ['attachments:get'](ctx, next) {
ctx.action.mergeParams({
appends: ['storage'],
});
await actions.get(ctx, async () => {
if (ctx.accepts('json', 'application/octet-stream') === 'json') {
return next();
}
const { body: attachment } = ctx;
const { storage } = attachment;
if (storage.type !== STORAGE_TYPE_LOCAL) {
return ctx.redirect(attachment.url);
}
ctx.body = fs.createReadStream(path.resolve(storage.options.documentRoot?, storage.path));
ctx.attachment(attachment.filename);
ctx.type = 'application/octet-stream';
await next();
});
}
});
```
Example request that will get the file stream:
```shell
curl -X GET -H "Accept: application/octet-stream" http://localhost:13000/api/attachments:get?filterByTk=1
```
### `create()`
Create a single piece of data. The URL for the corresponding resource action is `POST /api/<resource>:create`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `Object` | - | The data to create |
**Example**
Create data with binary content as attachment to the uploaded file, it is similar to the file management plugin:
```ts
import multer from '@koa/multer';
import actions from '@nocobase/actions';
app.actions({
async ['files:create'](ctx, next) {
if (ctx.request.type === 'application/json') {
return actions.create(ctx, next);
}
if (ctx.request.type !== 'multipart/form-data') {
return ctx.throw(406);
}
// Only use multer() as example here to save and process file, it does not represent the full logic
multer().single('file')(ctx, async () => {
const { file, body } = ctx.request;
const { filename, mimetype, size, path } = file;
ctx.action.mergeParams({
values: {
filename,
mimetype,
size,
path: file.path,
meta: typeof body.meta === 'string' ? JSON.parse(body.meta) : {};
}
});
await actions.create(ctx, next);
});
}
});
```
Example request to create plain data for a file table, you can submit it with an attachment:
```shell
# Create plain data only
curl -X POST -H "Content-Type: application/json" -d '{"filename": "some-file.txt", "mimetype": "text/plain", "size": 5, "url": "https://cdn.yourdomain.com/some-file.txt"}' "http://localhost:13000/api/files:create"
# Submit with attachment
curl -X POST -F "file=@/path/to/some-file.txt" -F 'meta={"length": 100}' "http://localhost:13000/api/files:create"
```
### `update()`
Update one or more pieces of data. The corresponding URL is `PUT /api/<resource>:update`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | Filtering parameter |
| `filterByTk` | `number \| string` | - | Filtering primary key |
| `values` | `Object` | - | Data values to update |
Note: Either or both `filter` or `filterByTk` should be provided.
**Example**
Similar to the example of `create()`, you can extend updating file record to a file that carries data with binary content:
```ts
import multer from '@koa/multer';
import actions from '@nocobase/actions';
app.actions({
async ['files:update'](ctx, next) {
if (ctx.request.type === 'application/json') {
return actions.update(ctx, next);
}
if (ctx.request.type !== 'multipart/form-data') {
return ctx.throw(406);
}
// Only use multer() as example here to save and process file, it does not represent the full logic
multer().single('file')(ctx, async () => {
const { file, body } = ctx.request;
const { filename, mimetype, size, path } = file;
ctx.action.mergeParams({
values: {
filename,
mimetype,
size,
path: file.path,
meta: typeof body.meta === 'string' ? JSON.parse(body.meta) : {};
}
});
await actions.update(ctx, next);
});
}
});
```
Example request to create plain data for a file table, you can submit it with an attachment:
```shell
# Create plain data only
curl -X PUT -H "Content-Type: application/json" -d '{"filename": "some-file.txt", "mimetype": "text/plain", "size": 5, "url": "https://cdn.yourdomain.com/some-file.txt"}' "http://localhost:13000/api/files:update"
# Submit with attachment
curl -X PUT -F "file=@/path/to/some-file.txt" -F 'meta={"length": 100}' "http://localhost:13000/api/files:update"
```
### `destroy()`
Delete one or more pieces of data. The corresponding URL is `DELETE /api/<resource>:destroy`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `filter` | `Filter` | - | Filtering parameter |
| `filterByTk` | `number \| string` | - | Filtering primary key |
Note: Either or both `filter` or `filterByTk` should be provided.
**Example**
Similar to the file management plug-in extension, a deletion of file data also requires the deletion of the corresponding file operation processing simultaneously:
```ts
import actions from '@nocobase/actions';
app.actions({
async ['files:destroy'](ctx, next) {
// const repository = getRepositoryFromParams(ctx);
// const { filterByTk, filter } = ctx.action.params;
// const items = await repository.find({
// fields: [repository.collection.filterTargetKey],
// appends: ['storage'],
// filter,
// filterByTk,
// context: ctx,
// });
// await items.reduce((promise, item) => promise.then(async () => {
// await item.removeFromStorage();
// await item.destroy();
// }), Promise.resolve());
await actions.destroy(ctx, async () => {
// do something
await next();
});
}
});
```
### `move()`
The corresponding URL is `POST /api/<resource>:move`.
This method is used to move data and adjust the order of data. For example, if you drag an element above or below another element in a page, you can call this method to achieve order adjustment.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `sourceId` | `targetKey` | - | ID of the element to move |
| `targetId` | `targetKey` | - | ID of the element to switch position with the moving element |
| `sortField` | `string` | `sort` | The stored field names of sorting |
| `targetScope` | `string` | - | The scope of sorting, a resource can be sorted by different scopes |
| `sticky` | `boolean` | - | Whether or not to top the moving element |
| `method` | `insertAfter` \| `prepend` | - | Type of insertion, before or after the target element |
## Resource Actions of Association Resource
### `add()`
Add an association to an object. The corresponding URL is `POST /api/<resource.assocition>:add`. Apply to `hasMany` and `belongsToMany` associations.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | ID of the association object to add |
### `remove()`
Remove the association to an object. The corresponding URL is `POST /api/<resource.assocition>:remove`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | ID of the associated object to remove |
### `set()`
Set the associated association object. The corresponding URL is `POST /api/<resource.assocition>:set`.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `TargetKey \| TargetKey[]` | - | ID of the association object to set |
### `toggle()`
Toggle the associated association object. The corresponding URL is `POST /api/<resource.assocition>:toggle`. `toggle` internally determines if the associated object already exists, removes it if it does, otherwise adds it.
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `values` | `TargetKey` | - | ID of the association object to toggle |

View File

@ -1,350 +0,0 @@
# @nocobase/cli
The NocoBase CLI is designed to help you develop, build, and deploy NocoBase applications.
<Alert>
NocoBase CLI supports <i>ts-node</i> and <i>node</i> two operation modes.
- ts-node mode (Default): Used for development environment, support real-time compilation, with relatively slow response
- node modeUsed for production environment, with quick response, but you need to execute `yarn nocobase build` to compile the entire source code first
</Alert>
## Instructions For Use
```bash
$ yarn nocobase -h
Usage: nocobase [command] [options]
Options:
-h, --help
Commands:
console
db:auth Verify if the database is successfully connected
db:sync Generate relevant data tables and fields through the configuration of collections
install Install
start Start application in production environment
build Compile and package
clean Delete the compiled files
dev Start application for development environment with real-time compilation
doc Documentation development
test Testing
umi
upgrade Upgrade
migrator Data migration
pm Plugin manager
help
```
## Application in Scaffolding
`scripts` in the application scaffolding `package.json` is as below:
```json
{
"scripts": {
"dev": "nocobase dev",
"start": "nocobase start",
"clean": "nocobase clean",
"build": "nocobase build",
"test": "nocobase test",
"pm": "nocobase pm",
"postinstall": "nocobase postinstall"
}
}
```
## Command Line Extensions
NocoBase CLI is built based on [commander](https://github.com/tj/commander.js). You can write the extended commands freely in `app/server/index.ts`:
```ts
const app = new Application(config);
app.command('hello').action(() => {});
```
or in the plugin:
```ts
class MyPlugin extends Plugin {
beforeLoad() {
this.app.command('hello').action(() => {});
}
}
```
Run in the terminal:
```bash
$ yarn nocobase hello
```
## Built-in Commands
Sorted by frequency of use.
### `dev`
Start application and compile code in real time in development environment.
<Alert>
NocoBase is installed automatically if it is not installed (Refer to the `install` command).
</Alert>
```bash
Usage: nocobase dev [options]
Options:
-p, --port [port]
--client
--server
-h, --help
```
Example:
```bash
# Launch application for development environment, with real-time compilation
yarn nocobase dev
# Start the server side only
yarn nocobase dev --server
# Start the client side only
yarn nocobase dev --client
```
### `start`
Start application in production environment, the code needs <i>yarn build</i>.
<Alert>
- NocoBase is installed automatically if it is not installed (Refer to the `install` command).
- The source code needs to be re-packaged if it has any modification (Refer to the `build` command).
</Alert>
```bash
$ yarn nocobase start -h
Usage: nocobase start [options]
Options:
-p, --port
-s, --silent
-h, --help
```
Example:
```bash
# Launch application for production environment
yarn nocobase start
```
### `install`
Install.
```bash
$ yarn nocobase install -h
Usage: nocobase install [options]
Options:
-f, --force
-c, --clean
-s, --silent
-l, --lang [lang]
-e, --root-email <rootEmail>
-p, --root-password <rootPassword>
-n, --root-nickname [rootNickname]
-h, --help
```
Example:
```bash
# Initial installation
yarn nocobase install -l zh-CN -e admin@nocobase.com -p admin123
# Delete all data tables from NocoBase and reinstall
yarn nocobase install -f -l zh-CN -e admin@nocobase.com -p admin123
# Clear database and reinstall
yarn nocobase install -c -l zh-CN -e admin@nocobase.com -p admin123
```
<Alert>
Difference between `-f/--force` and `-c/--clean`:
- `-f/--force` Delete data tables of NocoBase
- `-c/--clean` Clear database, all data tables are deleted
</Alert>
### `upgrade`
Upgrade.
```bash
yarn nocobase upgrade
```
### `test`
<i>jest</i> test, which supports all [jest-cli](https://jestjs.io/docs/cli) options, also supports `-c, --db-clean`.
```bash
$ yarn nocobase test -h
Usage: nocobase test [options]
Options:
-c, --db-clean Clear database before running all tests
-h, --help
```
Example:
```bash
# Run all test files
yarn nocobase test
# Run all test files in the specified folder
yarn nocobase test packages/core/server
# Run all tests in the specified file
yarn nocobase test packages/core/database/src/__tests__/database.test.ts
# Clear database before running all tests
yarn nocobase test -c
yarn nocobase test packages/core/server -c
```
### `build`
The source code needs to be compiled and packaged before the code is deployed to the production environment; and you need to re-build the code if it has any modification.
```bash
# All packages
yarn nocobase build
# Specified packages
yarn nocobase build app/server app/client
```
### `clean`
Delete the compiled files.
```bash
yarn clean
# Equivalent to
yarn rimraf -rf packages/*/*/{lib,esm,es,dist}
```
### `doc`
Documentation development.
```bash
# Start the documentation
yarn doc --lang=zh-CN # Equivalent to yarn doc dev
# Build the documentation, and output it to . /docs/dist/ directory by default
yarn doc build
# View the final result of the output documentation of dist
yarn doc serve --lang=zh-CN
```
### `db:auth`
Verify if the database is successfully connected.
```bash
$ yarn nocobase db:auth -h
Usage: nocobase db:auth [options]
Options:
-r, --retry [retry] Number of retries
-h, --help
```
### `db:sync`
Generate relevant data tables and fields through the configuration of collections.
```bash
$ yarn nocobase db:sync -h
Usage: nocobase db:sync [options]
Options:
-f, --force
-h, --help display help for command
```
### `migrator`
Data migration.
```bash
$ yarn nocobase migrator
Positional arguments:
<command>
up Applies pending migrations
down Revert migrations
pending Lists pending migrations
executed Lists executed migrations
create Create a migration file
```
### `pm`
Plugin manager.
```bash
# Create plugin
yarn pm create hello
# Register plugin
yarn pm add hello
# Enable plugin
yarn pm enable hello
# Disable plugin
yarn pm disable hello
# Remove plugin
yarn pm remove hello
```
Not achieved yet:
```bash
# Upgrade plugin
yarn pm upgrade hello
# Publish plugin
yarn pm publish hello
```
### `umi`
`app/client` is built based on [umi](https://umijs.org/), you can run other relevant commands through `nocobase umi`.
```bash
# Generate the .umi cache needed for the development environment
yarn nocobase umi generate tmp
```
### `help`
The help command, you can also use the option parameter, `-h` and `--help`.
```bash
# View all cli
yarn nocobase help
# Use -h instead
yarn nocobase -h
# Or --help
yarn nocobase --help
# View options of command db:sync
yarn nocobase db:sync -h
```

View File

@ -1,60 +0,0 @@
# Application
## Constructor
### `constructor()`
Create an application instance.
**Signature**
* `constructor(options: ApplicationOptions)`
**Example**
```ts
const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`../plugins/${name}`);
},
});
```
## Methods
### use()
Add Providers, build-in Providers are:
- APIClientProvider
- I18nextProvider
- AntdConfigProvider
- SystemSettingsProvider
- PluginManagerProvider
- SchemaComponentProvider
- BlockSchemaComponentProvider
- AntdSchemaComponentProvider
- ACLProvider
- RemoteDocumentTitleProvider
### render()
Component to render the App.
```ts
import { Application } from '@nocobase/client';
export const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`../plugins/${name}`);
},
});
export default app.render();
```

View File

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

View File

@ -1,25 +0,0 @@
# BlockProvider
## Kernel Methods
### `<BlockProvider />`
### `useBlockRequestContext()`
## Build-in BlockProvider Components
### `<CalendarBlockProvider />`
### `<TableFieldProvider />`
### `<TableBlockProvider />`
### `<TableSelectorProvider />`
### `<FormBlockProvider />`
### `<FormFieldProvider />`
### `<DetailsBlockProvider />`
### `<KanbanBlockProvider />`

View File

@ -1,268 +0,0 @@
# CollectionManager
## Components
### CollectionManagerProvider
```jsx | pure
<CollectionManagerProvider interfaces={{}} collections={[]}></CollectionManagerProvider>
```
### CollectionProvider
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}></CollectionProvider>
```
If there is no collection parameter passed in, get the collection from CollectionManagerProvider with the corresponding name.
```jsx | pure
const collections = [
{
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
}
];
<CollectionManagerProvider collections={collections}>
<CollectionProvider name={'tests'}></CollectionProvider>
</CollectionManagerProvider>
```
### CollectionFieldProvider
```jsx | pure
const field = {
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
};
<CollectionFieldProvider field={field}></CollectionFieldProvider>
```
If there is no field parameter passed in, get the field from CollectionProvider with the corresponding name.
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}>
<CollectionFieldProvider name={'title'}></CollectionFieldProvider>
</CollectionProvider>
```
### CollectionField
Universal field component that needs to be used with `<CollectionProvider/>`, but only in schema scenarios. Get the field schema from CollectionProvider with the corresponding name. Extend the configuration via the schema where the CollectionField is located.
```ts
{
name: 'title',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'CollectionField',
'x-component-props': {},
properties: {},
}
```
## Hooks
### useCollectionManager()
Use with `<CollectionManagerProvider/>`.
```jsx | pure
const { collections, get } = useCollectionManager();
```
### useCollection()
Use with `<CollectionProvider/>`.
```jsx | pure
const { name, fields, getField, findField, resource } = useCollection();
```
### useCollectionField()
Use with `<CollectionFieldProvider/>`.
```jsx | pure
const { name, uiSchema, resource } = useCollectionField();
```
The resource needs to be used with `<RecordProvider/>` to provide context of the record of the current data table row.
# CollectionManager
## Components
### CollectionManagerProvider
```jsx | pure
<CollectionManagerProvider interfaces={{}} collections={[]}></CollectionManagerProvider>
```
### CollectionProvider
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}></CollectionProvider>
```
If there is no collection parameter passed in, get the collection from CollectionManagerProvider with the corresponding name.
```jsx | pure
const collections = [
{
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
}
];
<CollectionManagerProvider collections={collections}>
<CollectionProvider name={'tests'}></CollectionProvider>
</CollectionManagerProvider>
```
### CollectionFieldProvider
```jsx | pure
const field = {
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
};
<CollectionFieldProvider field={field}></CollectionFieldProvider>
```
If there is no field parameter passed in, get the field from CollectionProvider with the corresponding name.
```jsx | pure
const collection = {
name: 'tests',
fields: [
{
type: 'string',
name: 'title',
interface: 'input',
uiSchema: {
type: 'string',
'x-component': 'Input'
},
},
],
};
<CollectionProvider collection={collection}>
<CollectionFieldProvider name={'title'}></CollectionFieldProvider>
</CollectionProvider>
```
### CollectionField
Universal field component that needs to be used with `<CollectionProvider/>`, but only in schema scenarios. Get the field schema from CollectionProvider with the corresponding name. Extend the configuration via the schema where the CollectionField is located.
```ts
{
name: 'title',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'CollectionField',
'x-component-props': {},
properties: {},
}
```
## Hooks
### useCollectionManager()
Use with `<CollectionManagerProvider/>`.
```jsx | pure
const { collections, get } = useCollectionManager();
```
### useCollection()
Use with `<CollectionProvider/>`.
```jsx | pure
const { name, fields, getField, findField, resource } = useCollection();
```
### useCollectionField()
Use with `<CollectionFieldProvider/>`.
```jsx | pure
const { name, uiSchema, resource } = useCollectionField();
```
The resource needs to be used with `<RecordProvider/>` to provide context of the record of the current data table row.

View File

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

View File

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

View File

@ -1,180 +0,0 @@
# Router
## API
### Initial
```tsx | pure
const app = new Application({
router: {
type: 'browser' // type default value is `browser`
}
})
// or
const app = new Application({
router: {
type: 'memory',
initialEntries: ['/']
}
})
```
### add Route
#### basic
```tsx | pure
import { RouteObject } from 'react-router-dom'
const app = new Application()
const Hello = () => {
return <div>Hello</div>
}
// first argument is `name` of route, second argument is `RouteObject`
app.router.add('root', {
path: '/',
element: <Hello />
})
app.router.add('root', {
path: '/',
Component: Hello
})
```
#### Component is String
```tsx | pure
app.addComponents({
Hello
})
app.router.add('root', {
path: '/',
Component: 'Hello'
})
```
#### nested
```tsx | pure
import { Outlet } from 'react-router-dom'
const Layout = () => {
return <div>
<Link to='/home'>Home</Link>
<Link to='/about'>about</Link>
<Outlet />
</div>
}
const Home = () => {
return <div>Home</div>
}
const About = () => {
return <div>About</div>
}
app.router.add('root', {
element: <Layout />
})
app.router.add('root.home', {
path: '/home',
element: <Home />
})
app.router.add('root.about', {
path: '/about',
element: <About />
})
```
It will generate the following routes:
```tsx | pure
{
element: <Layout />,
children: [
{
path: '/home',
element: <Home />
},
{
path: '/about',
element: <About />
}
]
}
```
### remove Route
```tsx | pure
// remove route by name
app.router.remove('root.home')
app.router.remove('hello')
```
#### Router in plugin
```tsx | pure
class MyPlugin extends Plugin {
async load() {
// add route
this.app.router.add('hello', {
path: '/hello',
element: <div>hello</div>,
})
// remove route
this.app.router.remove('world');
}
}
```
## Example
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { Link, Outlet } from 'react-router-dom';
import { Application } from '@nocobase/client';
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const Layout = () => {
return <div>
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
<Outlet />
</div>
}
const app = new Application({
router: {
type: 'memory',
initialEntries: ['/']
}
})
app.router.add('root', {
element: <Layout />
})
app.router.add('root.home', {
path: '/',
element: <Home />
})
app.router.add('root.about', {
path: '/about',
element: <About />
})
export default app.getRootComponent();
```

View File

@ -1,13 +0,0 @@
# SchemaComponent
## Core Components
### `<SchemaComponentProvider />`
### `<SchemaComponentOptions>`
### `<SchemaComponent>`
## Core Methods
### `createDesignable()`
### `useDesignable()`
### `useCompile()`

View File

@ -1,19 +0,0 @@
# SchemaInitializer
Used for the initialization of various schemas. Newly added schema can be inserted anywhere in an existing schema node, including:
```ts
{
properties: {
// beforeBegin - Insert in front of the current node
node1: {
properties: {
// afterBegin - Insert in front of the first child node of the current node
// ...
// beforeEnd - After the last child node of the current node
},
},
// afterEnd - After the current node
},
}
```

View File

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

View File

@ -1,510 +0,0 @@
# Collection
## Overview
`Collection` is used to define the data model in the system, such as model name, fields, indexes, associations, and other information. It is usually called through the `collection` method of the `Database` instance as a proxy entry.
```javascript
const { Database } = require('@nocobase/database')
// Create database instance
const db = new Database({...});
// Define data model
db.collection({
name: 'users',
// Define model fields
fields: [
// Scalar field
{
name: 'name',
type: 'string',
},
// Association field
{
name: 'profile',
type: 'hasOne' // 'hasMany', 'belongsTo', 'belongsToMany'
}
],
});
```
Refer to [Fields](/api/database/field.md) for more field types.
## Constructor
**Signature**
* `constructor(options: CollectionOptions, context: CollectionContext)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.name` | `string` | - | Identifier of the collection |
| `options.tableName?` | `string` | - | Database table name, the value of `options.name` is used if not set |
| `options.fields?` | `FieldOptions[]` | - | Definition of fields, refer to [Field](./field) for details |
| `options.model?` | `string \| ModelStatic<Model>` | - | Model type of Sequelize; in case `string` is used, this model name needs to be registered in the db before being called |
| `options.repository?` | `string \| RepositoryType` | - | Data repository type; in case `string` is used, this repository type needs to be registered in the db before being called |
| `options.sortable?` | `string \| boolean \| { name?: string; scopeKey?: string }` | - | Configure which fields are sortable; not sortable by default |
| `options.autoGenId?` | `boolean` | `true` | Whether to automatically generate unique primary key; `true` by default |
| `context.database` | `Database` | - | The context database in which it resides |
**Example**
Create a table <i>posts</i>:
```ts
const posts = new Collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'double',
name: 'price',
}
]
}, {
// An existing database instance
database: db
});
```
## Instance Members
### `options`
Initial parameters for data table configuration, which are consistent with the `options` parameter of the constructor.
### `context`
The contextual environment to which the current data table belongs, currently mainly the database instance.
### `name`
Name of the data table.
### `db`
The database instance to which it belongs.
### `filterTargetKey`
Name of the field that is used as the primary key.
### `isThrough`
Whether it is an intermediate table.
### `model`
Match the Model type of Sequelize.
### `repository`
Data repository instance.
## Field Configuration Methods
### `getField()`
Get a field object whose corresponding name has been defined in the data table.
**Signature**
* `getField(name: string): Field`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the field |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
const field = posts.getField('title');
```
### `setField()`
Set a field to the data table.
**Signature**
* `setField(name: string, options: FieldOptions): Field`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the field |
| `options` | `FieldOptions` | - | Configuration of the field, refer to [Field](./field) for details |
**Example**
```ts
const posts = db.collection({ name: 'posts' });
posts.setField('title', { type: 'string' });
```
### `setFields()`
Set multiple fields to the data table.
**Signature**
* `setFields(fields: FieldOptions[], resetFields = true): Field[]`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `fields` | `FieldOptions[]` | - | Configuration of the fields, refer to [Field](./field) for details |
| `resetFields` | `boolean` | `true` | Whether to reset existing fields |
**Example**
```ts
const posts = db.collection({ name: 'posts' });
posts.setFields([
{ type: 'string', name: 'title' },
{ type: 'double', name: 'price' }
]);
```
### `removeField()`
Remove a field object whose corresponding name has been defined in the data table.
**Signature**
* `removeField(name: string): void | Field`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the field |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.removeField('title');
```
### `resetFields()`
Reset (Empty) fields of the data table.
**Signature**
* `resetFields(): void`
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.resetFields();
```
### `hasField()`
Check if the data table has defined a field object with the corresponding name.
**Signature**
* `hasField(name: string): boolean`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the field |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.hasField('title'); // true
```
### `findField()`
Find field objects in the data table that match the conditions.
**Signature**
* `findField(predicate: (field: Field) => boolean): Field | undefined`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `predicate` | `(field: Field) => boolean` | - | The condition |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.findField(field => field.name === 'title');
```
### `forEachField()`
Iterate over field objects in the data table.
**Signature**
* `forEachField(callback: (field: Field) => void): void`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `callback` | `(field: Field) => void` | - | Callback function |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.forEachField(field => console.log(field.name));
```
## Index Configuration Methods
### `addIndex()`
Add data table index.
**Signature**
* `addIndex(index: string | string[] | { fields: string[], unique?: boolean,[key: string]: any })`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `index` | `string \| string[]` | - | Names of fields to be indexed |
| `index` | `{ fields: string[], unique?: boolean, [key: string]: any }` | - | Full configuration |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.addIndex({
fields: ['title'],
unique: true
});
```
### `removeIndex()`
Remove data table index.
**Signature**
* `removeIndex(fields: string[])`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `fields` | `string[]` | - | Names of fields to remove indexes |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
],
indexes: [
{
fields: ['title'],
unique: true
}
]
});
posts.removeIndex(['title']);
```
## Table Configuration Methods
### `remove()`
Remove data table.
**Signature**
* `remove(): void`
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
posts.remove();
```
## Database Operation Methods
### `sync()`
Synchronize the definitions in data table to the database. In addition to the default `Model.sync` logic in Sequelize, the data tables corresponding to the relational fields will also be handled together.
**Signature**
* `sync(): Promise<void>`
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
await posts.sync();
```
### `existsInDb()`
Check whether the data table exists in the database.
**Signature**
* `existsInDb(options?: Transactionable): Promise<boolean>`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options?.transaction` | `Transaction` | - | Transaction instance |
**Example**
```ts
const posts = db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
]
});
const existed = await posts.existsInDb();
console.log(existed); // false
```
### `removeFromDb()`
**Signature**
* `removeFromDb(): Promise<void>`
**Example**
```ts
const books = db.collection({
name: 'books'
});
// Synchronize the table books to the database
await db.sync();
// Remove the table books from the database
await books.removeFromDb();
```

View File

@ -1,558 +0,0 @@
# Field
## Overview
Data table field management class (abstract class). It is also the base class for all field types, and any other field types are implemented by inheriting from this class.
Refer to [Extended Field Types](/development/guide/collections-fields#extended-field-types) to see how to customize fields.
## Constructor
It is usually not called directly by the developer, but mainly through the `db.collection({ fields: [] })` method as a proxy entry.
Extended field is implemented mainly by inheriting the `Field` abstract class and registering it to a Database instance.
**Signature**
* `constructor(options: FieldOptions, context: FieldContext)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `FieldOptions` | - | Field configuration object |
| `options.name` | `string` | - | Field name |
| `options.type` | `string` | - | Field type, corresponding to the name of the field type registered in the db |
| `context` | `FieldContext` | - | Field context object |
| `context.database` | `Database` | - | Database instance |
| `context.collection` | `Collection` | - | Data table instance |
## Instance Members
### `name`
Field name.
### `type`
Field type.
### `dataType`
Data type of the field.
### `options`
Configuration parameters to initialize the field.
### `context`
Field context object.
## Configuration Methods
### `on()`
Quick definition method based on data table events. It is equivalent to `db.on(this.collection.name + '.' + eventName, listener)`.
It is usually not necessary to override this method when inheriting.
**Signature**
* `on(eventName: string, listener: (...args: any[]) => void)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `eventName` | `string` | - | Event name |
| `listener` | `(...args: any[]) => void` | - | Event listener |
### `off()`
Quick removal method based on data table events. It is equivalent to `db.off(this.collection.name + '.' + eventName, listener)`.
It is usually not necessary to override this method when inheriting.
**Signature**
* `off(eventName: string, listener: (...args: any[]) => void)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `eventName` | `string` | - | Event name |
| `listener` | `(...args: any[]) => void` | - | Event listener |
### `bind()`
The execution content that is triggered when a field is added to data table. Typically used to add data table event listeners and other processing.
The corresponding `super.bind()` method needs to be called first when inheriting.
**Signature**
* `bind()`
### `unbind()`
The execution content that is triggered when a field is removed from data table. Typically used to remove data table event listeners and other processing.
The corresponding `super.unbind()` method needs to be called first when inheriting.
**Signature**
* `unbind()`
### `get()`
Get the values of a configuration item of the field.
**Signature**
* `get(key: string): any`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` | - | Name of the configuration item |
**Example**
```ts
const field = db.collection('users').getField('name');
// Get and return the values of the configuration item 'name'
console.log(field.get('name'));
```
### `merge()`
Merge the values of a configuration item of the field.
**Signature**
* `merge(options: { [key: string]: any }): void`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `{ [key: string]: any }` | - | The configuration item to merge |
**Example**
```ts
const field = db.collection('users').getField('name');
field.merge({
// Add an index configuration
index: true
});
```
### `remove()`
Remove a field from data table (from memory only).
**Example**
```ts
const books = db.getCollections('books');
books.getField('isbn').remove();
// really remove from db
await books.sync();
```
## Database Methods
### `removeFromDb()`
Remove a field from the database.
**Signature**
* `removeFromDb(options?: Transactionable): Promise<void>`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.transaction?` | `Transaction` | - | Transaction instance |
### `existsInDb()`
Check if a field exists in the database.
**Signature**
* `existsInDb(options?: Transactionable): Promise<boolean>`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.transaction?` | `Transaction` | - | Transaction instance |
## Built-in Field Types
NocoBase has some built-in common field types, the corresponding type name can be used directly to specify the type of field upon definition. Fields of different types are configured differently, please refer to the list below.
The configuration items of all field types are passed through to Sequelize in addition to those described below. Therefore, all field configuration items supported by Sequelize can be used here (e.g. `allowNull`, `defaultValue`, etc.).
Moreover, server-side field types are mainly used for solving the problems of database storage and some algorithms, they are barely relevant to the field display types and the use of components in front-end. The front-end field types can be found in the corresponding tutorials.
### `'boolean'`
Boolean type.
**Example**
```js
db.collection({
name: 'books',
fields: [
{
type: 'boolean',
name: 'published'
}
]
});
```
### `'integer'`
Integer type (32 bits).
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'integer',
name: 'pages'
}
]
});
```
### `'bigInt'`
Long integer type (64 bits).
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'bigInt',
name: 'words'
}
]
});
```
### `'double'`
Double-precision floating-point format (64 bits).
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'double',
name: 'price'
}
]
});
```
### `'real'`
Real type (PG only).
### `'decimal'`
Decimal type.
### `'string'`
String type. Equivalent to the `VARCHAR` type for most databases.
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'string',
name: 'title'
}
]
});
```
### `'text'`
Text type. Equivalent to the `TEXT` type for most databases.
**Example**
```ts
db.collection({
name: 'books',
fields: [
{
type: 'text',
name: 'content'
}
]
});
```
### `'password'`
Password type (NocoBase extension). Password encryption based on the `scrypt` method of Node.js native crypto packages.
**Example**
```ts
db.collection({
name: 'users',
fields: [
{
type: 'password',
name: 'password',
length: 64, // Length, default is 64
randomBytesSize: 8 // Length of random bytes, default is 8
}
]
});
```
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `length` | `number` | 64 | Length of characters |
| `randomBytesSize` | `number` | 8 | Length of random bytes |
### `'date'`
Date type.
### `'time'`
Time type.
### `'array'`
Array type (PG only).
### `'json'`
JSON type.
### `'jsonb'`
JSONB type (PG only, others will be compatible with the `'json'` type).
### `'uuid'`
UUID type.
### `'uid'`
UID type (NocoBase extension). Short random string identifier type.
### `'formula'`
Formula type (NocoBase extension). Mathematical formula calculation can be configured based on [mathjs](https://www.npmjs.com/package/mathjs), and the formula can refer to the values of other columns in the same record to participate in the calculation.
**Example**
```ts
db.collection({
name: 'orders',
fields: [
{
type: 'double',
name: 'price'
},
{
type: 'integer',
name: 'quantity'
},
{
type: 'formula',
name: 'total',
expression: 'price * quantity'
}
]
});
```
### `'radio'`
Radio type (NocoBase extension). The field value is 'true' for at most one row of data for the full table, all others are 'false' or 'null'.
**Example**
There is only one user marked as <i>root</i> in the entire system, once the <i>root</i> value of any other user is changed to `true`, all other records with <i>root</i> of `true` will be changed to `false`:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'radio',
name: 'root',
}
]
});
```
### `'sort'`
Sorting type (NocoBase extension). Sorting based on integer numbers, automatically generating new serial numbers for new records, and rearranging serial numbers when moving data.
If data table has the `sortable` option defined, the corresponding fields will be generated automatically.
**Example**
Posts are sortable based on the users they belong to.
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'user',
},
{
type: 'sort',
name: 'priority',
scopeKey: 'userId' // Sort data grouped by the values of userId
}
]
});
```
### `'virtual'`
Virtual type. No Data is actually stored, it is used only when special getter/setter is defined.
### `'belongsTo'`
Many-to-one association type. Foreign key is stored in its own table, as opposed to `'hasOne'`/`'hasMany'`.
**Example**
Any post belongs to an author:
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'author',
target: 'users', // Default table name is the plural form of <name>
foreignKey: 'authorId', // Default is '<name> + Id'
sourceKey: 'id' // Default is id of the <target> table
}
]
});
```
### `'hasOne'`
One-to-one association type. Foreign key is stored in the association table, as opposed to `'belongsTo'`.
**Example**
Any user has a profile:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasOne',
name: 'profile',
target: 'profiles', // Can be omitted
}
]
})
```
### `'hasMany'`
One-to-many association type. The foreign key is stored in the association table, as opposed to `'belongsTo'`.
**Example**
Any user can have multiple posts:
```ts
db.collection({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'posts',
foreignKey: 'authorId',
sourceKey: 'id'
}
]
});
```
### `'belongsToMany'`
Many-to-many association type. Intermediate table is used to store both foreign keys. If no existing table is specified as intermediate table, it will be created automatically.
**Example**
Any post can have multiple tags added to it, and any tag can be added to multiple posts:
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'belongsToMany',
name: 'tags',
target: 'tags', // Can be omitted if name is the same
through: 'postsTags', // Intermediate table will be generated automatically if not specified
foreignKey: 'postId', // Foreign key in the intermediate table referring to the table itself
sourceKey: 'id', // Primary key of the table itself
otherKey: 'tagId' // Foreign key in the intermediate table referring to the association table
}
]
});
db.collection({
name: 'tags',
fields: [
{
type: 'belongsToMany',
name: 'posts',
through: 'postsTags', // Refer to the same intermediate table in the same set of relation
}
]
});
```

File diff suppressed because it is too large Load Diff

View File

@ -1,815 +0,0 @@
# Filter Operators
Used in the filter parameters of the `find`, `findOne`, `findAndCount`, `count`, etc. APIs of repository:
```ts
const repository = db.getRepository('books');
repository.find({
filter: {
title: {
$eq: 'Spring and Autumn',
}
}
});
```
To support JSON, NocoBase identifies query operators as a string prefixed with $.
Moreover, NocoBase provides API to extend operators. Refer to [`db.registerOperators()`](../database#registeroperators).
## General Operators
### `$eq`
Check if the field value is equal to the specified value. Equivalent to `=` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$eq: 'Spring and Autumn',
}
}
});
```
Equal to `title: 'Spring and Autumn'`
### `$ne`
Check if the field value is not equal to the specified value. Equivalent to `!=` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$ne: 'Spring and Autumn',
}
}
});
```
### `$is`
Check if the field value is the specified value. Equivalent to `IS` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$is: null,
}
}
});
```
### `$not`
Check if the field value is not the specified value. Equivalent to `IS NOT` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$not: null,
}
}
});
```
### `$col`
Check if the field value is equal to the value of another field. Equivalent to `=` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$col: 'name',
}
}
});
```
### `$in`
Check if the field value is in the specified array. Equivalent to `IN` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$in: ['Spring and Autumn', 'Warring States'],
}
}
});
```
### `$notIn`
Check if the field value is not in the specified array. Equivalent to `NOT IN` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$notIn: ['Spring and Autumn', 'Warring States'],
}
}
});
```
### `$empty`
Check if the general field is empty. For string field, check if it is an empty string; for array field, check if it is an empty array.
**Example**
```ts
repository.find({
filter: {
title: {
$empty: true,
}
}
});
```
### `$notEmpty`
Check if the general field is not empty. For string field, check if it is not an empty string; for array field, check if it is not an empty array.
**Example**
```ts
repository.find({
filter: {
title: {
$notEmpty: true,
}
}
});
```
## Logical Operators
### `$and`
Logical AND. Equivalent to `AND` in SQL.
**Example**
```ts
repository.find({
filter: {
$and: [
{ title: 'Book of Songs' },
{ isbn: '1234567890' },
]
}
});
```
### `$or`
Logical OR. Equivalent to `OR` in SQL.
**Example**
```ts
repository.find({
filter: {
$or: [
{ title: 'Book of Songs' },
{ publishedAt: { $lt: '0000-00-00T00:00:00Z' } },
]
}
});
```
## Boolean Field Operators
For boolean fields: `type: 'boolean'`
### `$isFalsy`
Check if a Boolean field value is false. Boolean field values of `false`, `0` and `NULL` are all judged to be `$isFalsy: true`.
**Example**
```ts
repository.find({
filter: {
isPublished: {
$isFalsy: true,
}
}
})
```
### `$isTruly`
Check if a Boolean field value is true. Boolean field values of `true` and `1` are all judged to be `$isTruly: true`.
**Example**
```ts
repository.find({
filter: {
isPublished: {
$isTruly: true,
}
}
})
```
## Numeric Type Field Operators
For numeric type fields, including:
- `type: 'integer'`
- `type: 'float'`
- `type: 'double'`
- `type: 'real'`
- `type: 'decimal'`
### `$gt`
Check if the field value is greater than the specified value. Equivalent to `>` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$gt: 100,
}
}
});
```
### `$gte`
Check if the field value is equal to or greater than the specified value. Equivalent to `>=` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$gte: 100,
}
}
});
```
### `$lt`
Check if the field value is less than the specified value. Equivalent to `<` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$lt: 100,
}
}
});
```
### `$lte`
Check if the field value is equal to or less than the specified value. Equivalent to `<=` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$lte: 100,
}
}
});
```
### `$between`
Check if the field value is between the specified two values. Equivalent to `BETWEEN` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$between: [100, 200],
}
}
});
```
### `$notBetween`
Check if the field value is not between the specified two values. Equivalent to `NOT BETWEEN` in SQL.
**Example**
```ts
repository.find({
filter: {
price: {
$notBetween: [100, 200],
}
}
});
```
## String Type Field Operators
For string type fields, including `string`.
### `$includes`
Check if the string field contains the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$includes: 'Three Character Classic',
}
}
})
```
### `$notIncludes`
Check if the string field does not contain the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$notIncludes: 'Three Character Classic',
}
}
})
```
### `$startsWith`
Check if the string field starts with the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$startsWith: 'Three Character Classic',
}
}
})
```
### `$notStatsWith`
Check if the string field does not start with the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$notStatsWith: 'Three Character Classic',
}
}
})
```
### `$endsWith`
Check if the string field ends with the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$endsWith: 'Three Character Classic',
}
}
})
```
### `$notEndsWith`
Check if the string field does not end with the specified substring.
**Example**
```ts
repository.find({
filter: {
title: {
$notEndsWith: 'Three Character Classic',
}
}
})
```
### `$like`
Check if the field value contains the specified string. Equivalent to `LIKE` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$like: 'Computer',
}
}
});
```
### `$notLike`
Check if the field value does not contain the specified string. Equivalent to `NOT LIKE` in SQL.
**Example**
```ts
repository.find({
filter: {
title: {
$notLike: 'Computer',
}
}
});
```
### `$iLike`
Check if a field value contains the specified string, case ignored. Equivalent to `ILIKE` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$iLike: 'Computer',
}
}
});
```
### `$notILike`
Check if a field value does not contain the specified string, case ignored. Equivalent to `NOT ILIKE` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$notILike: 'Computer',
}
}
});
```
### `$regexp`
Check if the field value matches the specified regular expression. Equivalent to `REGEXP` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$regexp: '^Computer',
}
}
});
```
### `$notRegexp`
Check if the field value does not match the specified regular expression. Equivalent to `NOT REGEXP` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$notRegexp: '^Computer',
}
}
});
```
### `$iRegexp`
Check if the field value matches the specified regular expression, case ignored. Equivalent to `~*` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$iRegexp: '^COMPUTER',
}
}
});
```
### `$notIRegexp`
Check if the field value does not match the specified regular expression, case ignored. Equivalent to `!~*` in SQL (PG only).
**Example**
```ts
repository.find({
filter: {
title: {
$notIRegexp: '^COMPUTER',
}
}
});
```
## Date Type Field Operators
For date type fields: `type: 'date'`
### `$dateOn`
Check if the date field value is within a certain day.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateOn: '2021-01-01',
}
}
})
```
### `$dateNotOn`
Check if the date field value is not within a certain day.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateNotOn: '2021-01-01',
}
}
})
```
### `$dateBefore`
Check if the date field value is before a certain value, i.e., less than the one passed in.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateBefore: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateNotBefore`
Check if the date field value is not before a certain value, i.e., equal to or greater than the one passed in.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateNotBefore: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateAfter`
Check if the date field value is after a certain value, i.e., greater than the one passed in.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateAfter: '2021-01-01T00:00:00.000Z',
}
}
})
```
### `$dateNotAfter`
Check if the date field value is not after a certain value, i.e., equal to or greater than the one passed in.
**Example**
```ts
repository.find({
filter: {
createdAt: {
$dateNotAfter: '2021-01-01T00:00:00.000Z',
}
}
})
```
## Array Type Field Operators
For array type fields: `type: 'array'`
### `$match`
Check if the array field values match values of the specified array.
**Example**
```ts
repository.find({
filter: {
tags: {
$match: ['literature', 'history'],
}
}
})
```
### `$notMatch`
Check if the array field values do not match values of the specified array.
**Example**
```ts
repository.find({
filter: {
tags: {
$notMatch: ['literature', 'history'],
}
}
})
```
### `$anyOf`
Check if the array field values contain any of the values of the specified array.
**Example**
```ts
repository.find({
filter: {
tags: {
$anyOf: ['literature', 'history'],
}
}
})
```
### `$noneOf`
Check if the array field values contain none of the values of the specified array.
**Example**
```ts
repository.find({
filter: {
tags: {
$noneOf: ['literature', 'history'],
}
}
})
```
### `$arrayEmpty`
Check if the array field is empty.
**Example**
```ts
repository.find({
filter: {
tags: {
$arrayEmpty: true,
}
}
});
```
### `$arrayNotEmpty`
Check if the array field is not empty.
**Example**
```ts
repository.find({
filter: {
tags: {
$arrayNotEmpty: true,
}
}
});
```
## Relational Field Type Operators
For checking if a relationship exists, field types include:
- `type: 'hasOne'`
- `type: 'hasMany'`
- `type: 'belongsTo'`
- `type: 'belongsToMany'`
### `$exists`
There is relational data existing.
**Example**
```ts
repository.find({
filter: {
author: {
$exists: true,
}
}
});
```
### `$notExists`
There is no relational data existing.
**Example**
```ts
repository.find({
filter: {
author: {
$notExists: true,
}
}
});
```

View File

@ -1,186 +0,0 @@
# BelongsToManyRepository
`BelongsToManyRepository` is the `Relation Repository` for handling `BelongsToMany` relationships.
Unlike other relationship types, the `BelongsToMany` type of relationship needs to be recorded through an intermediate table. The intermediate table can be created automatically or explicitly specified when defining association relationships in NocoBase.
## Class Methods
### `find()`
Find associated objects.
**Signature**
* `async find(options?: FindOptions): Promise<M[]>`
**Detailed Information**
Query parameters are the same as [`Repository.find()`](../repository.md#find).
### `findOne()`
Find associated objects, only to return one record.
**Signature**
* `async findOne(options?: FindOneOptions): Promise<M>`
<embed src="../shared/find-one.md"></embed>
### `count()`
Return the number of records matching the query criteria.
**Signature**
* `async count(options?: CountOptions)`
**Type**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
### `findAndCount()`
Find datasets from the database with the specified filtering conditions and return the number of results.
**Signature**
* `async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>`
**Type**
```typescript
type FindAndCountOptions = CommonFindOptions
```
### `create()`
Create associated objects.
**Signature**
* `async create(options?: CreateOptions): Promise<M>`
<embed src="../shared/create-options.md"></embed>
### `update()`
Update associated objects that match the conditions.
**Signature**
* `async update(options?: UpdateOptions): Promise<M>`
<embed src="../shared/update-options.md"></embed>
### `destroy()`
Delete associated objects.
**Signature**
* `async destroy(options?: TargetKey | TargetKey[] | DestroyOptions): Promise<Boolean>`
<embed src="../shared/destroy-options.md"></embed>
### `add()`
Add new associated objects.
**Signature**
* `async add(
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions
): Promise<void>`
**Type**
```typescript
type PrimaryKeyWithThroughValues = [TargetKey, Values];
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[];
}
```
**Detailed Information**
Pass the `targetKey` of the associated object directly, or pass the `targetKey` along with the field values of the intermediate table.
**Example**
```typescript
const t1 = await Tag.repository.create({
values: { name: 't1' },
});
const t2 = await Tag.repository.create({
values: { name: 't2' },
});
const p1 = await Post.repository.create({
values: { title: 'p1' },
});
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
// Pass in the targetKey
PostTagRepository.add([
t1.id, t2.id
]);
// Pass in intermediate table fields
PostTagRepository.add([
[t1.id, { tagged_at: '123' }],
[t2.id, { tagged_at: '456' }],
]);
```
### `set()`
Set the associated objects.
**Signature**
* async set(
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions,
): Promise<void>
**Detailed Information**
Parameters are the same as [add()](#add).
### `remove()`
Remove the association with the given objects.
**Signature**
* `async remove(options: TargetKey | TargetKey[] | AssociatedOptions)`
**Type**
```typescript
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[];
}
```
### `toggle()`
Toggle the associated object.
In some business scenarios, it is often needed to toggle the associated object. For example, user adds a product into collection, and the user cancels the collection and collect it again. Using the `toggle` method can quickly implement similar functions.
**Signature**
* `async toggle(options: TargetKey | { tk?: TargetKey; transaction?: Transaction }): Promise<void>`
**Detailed Information**
The `toggle` method automatically checks whether the associated object already exists, and removes it if it does, or adds it if it does not.

View File

@ -1,3 +0,0 @@
## BelongsToRepository
The interface is the same as [HasOneRepository](./has-one-repository.md). `BelongsToRepository` is the `Repository` for handling `BelongsTo` relationships, and it provides some convenient methods to handle `BelongsTo` relationships.

View File

@ -1,133 +0,0 @@
# HasManyRepository
`HasManyRepository` is the `Relation Repository` for handling `HasMany` relationships.
## Class Methods
### `find()`
Find associated objects.
**Signature**
* `async find(options?: FindOptions): Promise<M[]>`
**Detailed Information**
Query parameters are the same as [`Repository.find()`](../repository.md#find).
### `findOne()`
Find associated objects, only to return one record.
**Signature**
* `async findOne(options?: FindOneOptions): Promise<M>`
<embed src="../shared/find-one.md"></embed>
### `count()`
Return the number of records matching the query criteria.
**Signature**
* `async count(options?: CountOptions)`
**Type**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
### `findAndCount()`
Find datasets from the database with the specified filtering conditions and return the number of results.
**Signature**
* `async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]>`
**Type**
```typescript
type FindAndCountOptions = CommonFindOptions
```
### `create()`
Create associated objects.
**Signature**
* `async create(options?: CreateOptions): Promise<M>`
<embed src="../shared/create-options.md"></embed>
### `update()`
Update associated objects that match the conditions.
**Signature**
* `async update(options?: UpdateOptions): Promise<M>`
<embed src="../shared/update-options.md"></embed>
### `destroy()`
Delete associated objects.
**Signature**
* `async destroy(options?: TK | DestroyOptions): Promise<M>`
<embed src="../shared/destroy-options.md"></embed>
### `add()`
Add association relationships between objects.
**Signature**
* `async add(options: TargetKey | TargetKey[] | AssociatedOptions)`
**Type**
```typescript
interface AssociatedOptions extends Transactionable {
tk?: TargetKey | TargetKey[];
}
```
**Detailed Information**
* `tk` - The targetKey value of the associated object, either as a single value or an array.
<embed src="../shared/transaction.md"></embed>
### `remove()`
Remove the association with the given objects.
**Signature**
* `async remove(options: TargetKey | TargetKey[] | AssociatedOptions)`
**Detailed Information**
Same parameters as the [`add()`](#add) method.
### `set()`
Set the associated object of the current relationship.
**Signature**
* `async set(options: TargetKey | TargetKey[] | AssociatedOptions)`
**Detailed Information**
Same parameters as the [`add()`](#add) method.

View File

@ -1,184 +0,0 @@
# HasOneRepository
## Overview
`HasOneRepository` is the associated repository of type `HasOne`.
```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' },
});
// Get the associated repository
const userProfileRepository = User.repository.relation('profile').of(user.get('id'));
// Or to initialize directly
new HasOneRepository(User, 'profile', user.get('id'));
```
## Class Methods
### `find()`
Find associated objects.
**Signature**
* `async find(options?: SingleRelationFindOption): Promise<Model<any> | null>`
**Type**
```typescript
interface SingleRelationFindOption extends Transactionable {
fields?: Fields;
except?: Except;
appends?: Appends;
filter?: Filter;
}
```
**Detailed Information**
Query parameters are the same as [`Repository.find()`](../repository.md#find).
**Example**
```typescript
const profile = await UserProfileRepository.find();
// Return null if the associated object does not exist
```
### `create()`
Create associated objects.
**Signature**
* `async create(options?: CreateOptions): Promise<Model>`
<embed src="../shared/create-options.md"></embed>
**Example**
```typescript
const profile = await UserProfileRepository.create({
values: { avatar: 'avatar1' },
});
console.log(profile.toJSON());
/*
{
id: 1,
avatar: 'avatar1',
userId: 1,
updatedAt: 2022-09-24T13:59:40.025Z,
createdAt: 2022-09-24T13:59:40.025Z
}
*/
```
### `update()`
Update associated objects.
**Signature**
* `async update(options: UpdateOptions): Promise<Model>`
<embed src="../shared/update-options.md"></embed>
**Example**
```typescript
const profile = await UserProfileRepository.update({
values: { avatar: 'avatar2' },
});
profile.get('avatar'); // 'avatar2'
```
### `remove()`
Remove associated objects. Only to unassociate, not to delete the associated object.
**Signature**
* `async remove(options?: Transactionable): Promise<void>`
**Detailed Information**
* `transaction`: Transaction object. If no transaction parameter is passed, the method will automatically create an internal transaction.
**Example**
```typescript
await UserProfileRepository.remove();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 1; // true
```
### `destroy()`
Delete associated objects.
**Signature**
* `async destroy(options?: Transactionable): Promise<Boolean>`
**Detailed Information**
* `transaction`: Transaction object. If no transaction parameter is passed, the method will automatically create an internal transaction.
**Example**
```typescript
await UserProfileRepository.destroy();
await UserProfileRepository.find() == null; // true
await Profile.repository.count() === 0; // true
```
### `set()`
Set associated objects.
**Signature**
* `async set(options: TargetKey | SetOption): Promise<void>`
**Type**
```typescript
interface SetOption extends Transactionable {
tk?: TargetKey;
}
````
**Detailed Information**
* tk: Set the targetKey of the associated object.
* transaction: Transaction object. If no transaction parameter is passed, the method will automatically create an internal transaction.
**Example**
```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

@ -1,45 +0,0 @@
# 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,684 +0,0 @@
# Repository
## Overview
On a given `Collection` object, you can get its `Repository` object to perform read and write operations on the data table.
```javascript
const { UserCollection } = require("./collections");
const UserRepository = UserCollection.repository;
const user = await UserRepository.findOne({
filter: {
id: 1
},
});
user.name = "new name";
await user.save();
```
### Query
#### Basic Query
On the `Repository` object, call the `find*` methods to perform query. The `filter` parameter is supported by all query methods to filter the data.
```javascript
// SELECT * FROM users WHERE id = 1
userRepository.find({
filter: {
id: 1
}
});
```
#### Operator
The `filter` parameter in the `Repository` also provides a variety of operators to perform more diverse queries.
```javascript
// SELECT * FROM users WHERE age > 18
userRepository.find({
filter: {
age: {
$gt: 18
}
}
});
// SELECT * FROM users WHERE age > 18 OR name LIKE '%张%'
userRepository.find({
filter: {
$or: [
{ age: { $gt: 18 } },
{ name: { $like: "%张%" } }
]
}
});
```
Refer to [Filter Operators](/api/database/operators) for more details on operators.
#### Field Control
Control the output fields by the `fields`, `except`, and `appends` parameters when performing query.
* `fields`: Specify output fields
* `except`: Exclude output fields
* `appends`: Append output associated fields
```javascript
// The result contains only the id and name fields
userRepository.find({
fields: ["id", "name"],
});
// The result does not contain only the password field
userRepository.find({
except: ["password"],
});
// The result contains data associated with the posts object
userRepository.find({
appends: ["posts"],
});
```
#### Associated Field Query
The `filter` parameter supports filtering by associated fields, for example:
```javascript
// Find the user objects whose associated posts have title of "post title"
userRepository.find({
filter: {
"posts.title": "post title"
}
});
```
Associated fields can also be nested:
```javascript
// Find the user objects whose associated posts have comments containing "keywords"
await userRepository.find({
filter: {
"posts.comments.content": {
$like: "%keywords%"
}
}
});
```
#### Sort
Sort query results by the `sort` parameter.
```javascript
// SELECT * FROM users ORDER BY age
await userRepository.find({
sort: 'age'
});
// SELECT * FROM users ORDER BY age DESC
await userRepository.find({
sort: '-age'
});
// SELECT * FROM users ORDER BY age DESC, name ASC
await userRepository.find({
sort: ['-age', "name"],
});
```
Sort by the field of the associated object is also supported:
```javascript
await userRepository.find({
sort: 'profile.createdAt'
});
```
### Create
#### Basic Create
Create new data objects via `Repository`.
```javascript
await userRepository.create({
name: "Mark",
age: 18,
});
// INSERT INTO users (name, age) VALUES ('Mark', 18)
// Bulk creation
await userRepository.create([
{
name: "Mark",
age: 18,
},
{
name: "Alex",
age: 20,
},
])
```
#### Create Association
Create associated objects at the same time of creating data. Like query, nested use of associated objects is also supported. For example:
```javascript
await userRepository.create({
name: "Mark",
age: 18,
posts: [
{
title: "post title",
content: "post content",
tags: [
{
name: "tag1",
},
{
name: "tag2",
},
],
},
],
});
// When creating a user, create a post to associate with the user, and create tags to associate with the post
```
If the associated object is already in the database, you can pass its ID to create an association with it.
```javascript
const tag1 = await tagRepository.findOne({
filter: {
name: "tag1"
},
});
await userRepository.create({
name: "Mark",
age: 18,
posts: [
{
title: "post title",
content: "post content",
tags: [
{
id: tag1.id, // Create an association with an existing associated object
},
{
name: "tag2",
},
],
},
],
});
```
### Update
#### Basic Update
After getting the data object, you can modify the properties directly on the data object (`Model`), and then call the `save` method to save the changes.
```javascript
const user = await userRepository.findOne({
filter: {
name: "Mark",
},
});
user.age = 20;
await user.save();
```
The data object `Model` is inherited from Sequelize Model, refer to [Sequelize Model](https://sequelize.org/master/manual/model-basics.html) for the operations on `Model`.
Or update data via `Repository`:
```javascript
// Update the records that meet the filtering condition
await userRepository.update({
filter: {
name: "Mark",
},
values: {
age: 20,
},
});
```
Control which fields to update by the `whitelist` and `blacklist` parameters, for example:
```javascript
await userRepository.update({
filter: {
name: "Mark",
},
values: {
age: 20,
name: "Alex",
},
whitelist: ["age"], // Only update the age field
});
````
#### Update Associated Field
Associated objects can be set while updating, for example:
```javascript
const tag1 = tagRepository.findOne({
filter: {
id: 1
},
});
await postRepository.update({
filter: {
id: 1
},
values: {
title: "new post title",
tags: [
{
id: tag1.id // Associate with tag1
},
{
name: "tag2", // Create new tag and associate with it
},
],
},
});
await postRepository.update({
filter: {
id: 1
},
values: {
tags: null // Disassociate post from tags
},
})
```
### Delete
Call the `destroy()` method in `Repository` to perform the deletion operation. Filtering condition has to be specified to delete.
```javascript
await userRepository.destroy({
filter: {
status: "blocked",
},
});
```
## Constructor
It is usually not called directly by the developer, the instantiation is done mainly by specifying a corresponding repository type that is already registered in the parameter of `db.colletion()`. Repository type is registered through `db.registerRepositories()`.
**Signature**
* `constructor(collection: Collection)`
**Example**
```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;');
```
## Instance Members
### `database`
The database management instance where the context is located.
### `collection`
The corresponding data table management instance.
### `model`
The corresponding data model class.
## Instance Methods
### `find()`
Find datasets from the database with the specified filtering conditions and sorting, etc.
**Signature**
* `async find(options?: FindOptions): Promise<Model[]>`
**Type**
```typescript
type Filter = FilterWithOperator | FilterWithValue | FilterAnd | FilterOr;
type Appends = string[];
type Except = string[];
type Fields = string[];
type Sort = string[] | string;
interface SequelizeFindOptions {
limit?: number;
offset?: number;
}
interface FilterByTk {
filterByTk?: TargetKey;
}
interface CommonFindOptions extends Transactionable {
filter?: Filter;
fields?: Fields;
appends?: Appends;
except?: Except;
sort?: Sort;
}
type FindOptions = SequelizeFindOptions & CommonFindOptions & FilterByTk;
```
**Detailed Information**
#### `filter: Filter`
Query conditions for filtering data results. In the query parameters that passed in, `key` is the name of the field, `value` is the corresponding value. Operators can be used in conjunction with other filtering conditions.
```typescript
// Find records with name "foo" and age above 18
repository.find({
filter: {
name: "foo",
age: {
$gt: 18,
},
}
})
```
Refer to [Operators](./operators.md) for more information.
#### `filterByTk: TargetKey`
Query data by `TargetKey`, this is shortcut for the `filter` parameter. The field of `TargetKey` can be [configured](./collection.md#filtertargetkey) in `Collection`, the default is `primaryKey`.
```typescript
// By default, find records with id 1
repository.find({
filterByTk: 1,
});
```
#### `fields: string[]`
Query columns. It is used to control which data fields to output. With this parameter, only the specified fields will be returned.
#### `except: string[]`
Exclude columns. It is used to control which data fields to output. With this parameter, the specified fields will not be returned.
#### `appends: string[]`
Append columns. It is used to load associated data. With this parameter, the specified associated fields will be returned together.
#### `sort: string[] | string`
Specify the sorting method of the query results. The input parameter is the name of the field, by default is to sort in the ascending order (`asc`); a `-` symbol needs to be added before the field name to sort in the descending order (`desc`). For example, `['-id', 'name']` means to sort by `id desc, name asc`.
#### `limit: number`
Limit the number of results, same as `limit` in `SQL`.
#### `offset: number`
The offset of the query, same as `offset` in `SQL`.
**Example**
```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()`
Find a single piece of data from the database for specific conditions. Equivalent to `Model.findOne()` in Sequelize.
**Signature**
* `async findOne(options?: FindOneOptions): Promise<Model | null>`
<embed src="./shared/find-one.md"></embed>
**Example**
```ts
const posts = db.getRepository('posts');
const result = await posts.findOne({
filterByTk: 1,
});
```
### `count()`
Query a certain amount of data from the database for specific conditions. Equivalent to `Model.count()` in Sequelize.
**Signature**
* `count(options?: CountOptions): Promise<number>`
**Type**
```typescript
interface CountOptions extends Omit<SequelizeCountOptions, 'distinct' | 'where' | 'include'>, Transactionable {
filter?: Filter;
}
```
**Example**
```ts
const books = db.getRepository('books');
const count = await books.count({
filter: {
title: 'Three character classic'
}
});
```
### `findAndCount()`
Find datasets from the database with the specified filtering conditions and return the number of results. Equivalent to `Model.findAndCountAll()` in Sequelize.
**Signature**
* `async findAndCount(options?: FindAndCountOptions): Promise<[Model[], number]>`
**Type**
```typescript
type FindAndCountOptions = Omit<SequelizeAndCountOptions, 'where' | 'include' | 'order'> & CommonFindOptions;
```
**Detailed Information**
The query parameters are the same as `find()`. An array is returned with the first element of the query results, and the second element of the total number of results.
### `create()`
Inserts a newly created data into the data table. Equivalent to `Model.create()` in Sequelize. When the data object to be created carries any associated field, the corresponding associated data record is created or updated along with it.
**Signature**
* `async create<M extends Model>(options: CreateOptions): Promise<M>`
<embed src="./shared/create-options.md"></embed>
**Example**
```ts
const posts = db.getRepository('posts');
const result = await posts.create({
values: {
title: 'NocoBase 1.0 Release Notes',
tags: [
// Update data when there is a primary key and value of the associated table
{ id: 1 },
// Create data when there is no primary key and value
{ name: 'NocoBase' },
]
},
});
```
### `createMany()`
Inserts multiple newly created data into the data table. This is equivalent to calling the `create()` method multiple times.
**Signature**
* `createMany(options: CreateManyOptions): Promise<Model[]>`
**Type**
```typescript
interface CreateManyOptions extends BulkCreateOptions {
records: Values[];
}
```
**Detailed Information**
* `records`: An array of data objects to be created.
* `transaction`: Transaction object. If no transaction parameter is passed, the method will automatically create an internal transaction.
**Example**
```ts
const posts = db.getRepository('posts');
const results = await posts.createMany({
records: [
{
title: 'NocoBase 1.0 Release Notes',
tags: [
// Update data when there is a primary key and value of the associated table
{ id: 1 },
// Create data when there is no primary key and value
{ name: 'NocoBase' },
]
},
{
title: 'NocoBase 1.1 Release Notes',
tags: [
{ id: 1 }
]
},
],
});
```
### `update()`
Update data in the data table. Equivalent to `Model.update()` in Sequelize. When the data object to be updated carries any associated field, the corresponding associated data record is created or updated along with it.
**Signature**
* `async update<M extends Model>(options: UpdateOptions): Promise<M>`
<embed src="./shared/update-options.md"></embed>
**Example**
```ts
const posts = db.getRepository('posts');
const result = await posts.update({
filterByTk: 1,
values: {
title: 'NocoBase 1.0 Release Notes',
tags: [
// Update data when there is a primary key and value of the associated table
{ id: 1 },
// Create data when there is no primary key and value
{ name: 'NocoBase' },
]
},
});
```
### `destroy()`
Delete data from the data table. Equivalent to `Model.destroy()` in Sequelize.
**Signature**
* `async destroy(options?: TargetKey | TargetKey[] | DestoryOptions): Promise<number>`
**Type**
```typescript
interface DestroyOptions extends SequelizeDestroyOptions {
filter?: Filter;
filterByTk?: TargetKey | TargetKey[];
truncate?: boolean;
context?: any;
}
```
**Detailed Information**
* `filter`Specify the filtering conditions of the records to be deleted. Refer to the [`find()`](#find) method for the detailed usage of the filter.
* `filterByTk`Specify the filtering conditions by TargetKey.
* `truncate`: Whether to empty the table data, this parameter is valid if no `filter` or `filterByTk` parameter is passed.
* `transaction`: Transaction object. If no transaction parameter is passed, the method will automatically create an internal transaction.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,236 +0,0 @@
# Environment Variables
## Global Environment Variables
Saved in the `.env` file
### APP_ENV
Application environment, default is `development`, options include
- `production` production environment
- `development` development environment
```bash
APP_ENV=production
```
### APP_HOST
Application host, default is `0.0.0.0`
```bash
APP_HOST=192.168.3.154
```
### APP_PORT
Application port, default is `13000`
```bash
APP_PORT=13000
```
### APP_KEY
Secret key, for scenarios such as jwt
```bash
APP_KEY=app-key-test
```
### API_BASE_PATH
NocoBase API address prefix, default is `/api/`
```bash
API_BASE_PATH=/api/
```
### PLUGIN_PACKAGE_PREFIX
Plugin package prefix, default is `@nocobase/plugin-,@nocobase/preset-`
For example, add plugin `hello` into project `my-nocobase-app`, the plugin package name is `@my-nocobase-app/plugin-hello`.
PLUGIN_PACKAGE_PREFIX is configured as follows:
```bash
PLUGIN_PACKAGE_PREFIX=@nocobase/plugin-,@nocobase-preset-,@my-nocobase-app/plugin-
```
The correspondence between plugin name and package name is:
- `users` plugin package name is `@nocobase/plugin-users`
- `nocobase` plugin package name is `@nocobase/preset-nocobase`
- `hello` plugin package name is `@my-nocobase-app/plugin-hello`
### DB_DIALECT
Database type, default is `sqlite`, options include
- `sqlite`
- `mysql`
- `postgres`
```bash
DB_DIALECT=mysql
```
### DB_STORAGE
Database file path (required when using a SQLite database)
```bash
### Relative path
DB_HOST=storage/db/nocobase.db
# Absolute path
DB_HOST=/your/path/nocobase.db
```
### DB_HOST
Database host (required when using MySQL or PostgreSQL databases)
Default is `localhost`
```bash
DB_HOST=localhost
```
### DB_PORT
Database port (required when using MySQL or PostgreSQL databases)
- Default port of MySQL is 3306
- Default port of PostgreSQL is 5432
```bash
DB_PORT=3306
```
### DB_DATABASE
Database name (required when using MySQL or PostgreSQL databases)
```bash
DB_DATABASE=nocobase
```
### DB_USER
Database user (required when using MySQL or PostgreSQL databases)
```bash
DB_USER=nocobase
```
### DB_PASSWORD
Database password (required when using MySQL or PostgreSQL databases)
```bash
DB_PASSWORD=nocobase
```
### DB_TABLE_PREFIX
Data table prefix
```bash
DB_TABLE_PREFIX=nocobase_
```
### DB_LOGGING
Database log switch, default is `off`, options include
- `on` on
- `off` off
```bash
DB_LOGGING=on
```
### LOGGER_TRANSPORT
Log transport, default is `console,dailyRotateFile`, options include
- `console`
- `dailyRotateFile`
### LOGGER_BASE_PATH
Base path to save file based logs, default is `storage/logs`
## Temporary Environment Variables
The installation of NocoBase can be assited by setting temporary environment variables, such as:
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
# Equivalent to
yarn nocobase install \
--lang=zh-CN \
--root-email=demo@nocobase.com \
--root-password=admin123 \
--root-nickname="Super Admin"
# Equivalent to
yarn nocobase install -l zh-CN -e demo@nocobase.com -p admin123 -n "Super Admin"
```
### INIT_APP_LANG
Language at the time of installation, default is `en-US`, options include
- `en-US` English
- `zh-CN` Chinese (Simplified)
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
nocobase install
```
### INIT_ROOT_EMAIL
Root user mailbox
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
nocobase install
```
### INIT_ROOT_PASSWORD
Root user password
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
nocobase install
```
### INIT_ROOT_NICKNAME
Root user nickname
```bash
yarn cross-env \
INIT_APP_LANG=zh-CN \
INIT_ROOT_EMAIL=demo@nocobase.com \
INIT_ROOT_PASSWORD=admin123 \
INIT_ROOT_NICKNAME="Super Admin" \
nocobase install
```

View File

@ -1,301 +0,0 @@
# Overview
HTTP API of NocoBase is designed based on Resource & Action, a superset of REST API. The operation includes but not limited to create, read, update and delete. Resource Action can be extended arbitrarily in NocoBase.
## Resource
In NocoBase, resource has two expressions:
- `<collection>`
- `<collection>.<association>`
<Alert>
- Collection is the set of all abstract data
- Association is the associated data of collection
- Resource includes both collection and collection.association
</Alert>
### Example
- `posts` Post
- `posts.user` Post user
- `posts.tags` Post tags
## Action
Action on resource is expressed by `:<action>`
- `<collection>:<action>`
- `<collection>.<association>:<action>`
Built-in global actions for collection or association:
- `create`
- `get`
- `list`
- `update`
- `destroy`
- `move`
Built-in association actions for association only:
- `set`
- `add`
- `remove`
- `toggle`
### Example
- `posts:create` Create post
- `posts.user:get` Get post user
- `posts.tags:add` Add tags to post (associate existing tags with post)
## Request URL
```bash
<GET|POST> /api/<collection>:<action>
<GET|POST> /api/<collection>:<action>/<collectionIndex>
<GET|POST> /api/<collection>/<collectionIndex>/<association>:<action>
<GET|POST> /api/<collection>/<collectionIndex>/<association>:<action>/<associationIndex>
```
### Example
posts resource
```bash
POST /api/posts:create
GET /api/posts:list
GET /api/posts:get/1
POST /api/posts:update/1
POST /api/posts:destroy/1
```
posts.comments resource
```bash
POST /api/posts/1/comments:create
GET /api/posts/1/comments:list
GET /api/posts/1/comments:get/1
POST /api/posts/1/comments:update/1
POST /api/posts/1/comments:destroy/1
```
posts.tags resource
```bash
POST /api/posts/1/tags:create
GET /api/posts/1/tags:get
GET /api/posts/1/tags:list
POST /api/posts/1/tags:update
POST /api/posts/1/tags:destroy
POST /api/posts/1/tags:add
GET /api/posts/1/tags:remove
```
## Locate Resource
- Collection resource locates the data to be processed by `collectionIndex`, `collectionIndex` must be unique.
- Association resource locates the data to be processed by `collectionIndex` and `associationIndex` jointly, `associationIndex` may not be unique, but the joint index of `collectionIndex` and `associationIndex` must be unique.
When viewing details of association resource, the requested URL needs to provide both `<collectionIndex>` and `<associationIndex>`, `<collectionIndex>` is necessary as `<associationIndex>` may not be unique.
For example, `tables.fields` represents the fields of a data table:
```bash
GET /api/tables/table1/fields/title
GET /api/tables/table2/fields/title
```
Both table1 and table2 have the title field, title is unique in one table, but other tables may also have fields of that name.
## Request Parameters
Request parameters can be placed in the headers, parameters (query string), and body (GET requests do not have a body) of the request.
Some special request parameters:
- `filter` Data filtering, used in actions related to query.
- `filterByTk` Filter by tk field, used in actions to specify details of data.
- `sort` Sorting, used in actions related to query.
- `fields` Date to output, used in actions related to query
- `appends` Fields of additional relationship, used in actions related to query.
- `except` Exclude some fields (not to output), used in actions related to query.
- `whitelist` Fields whitelist, used in actions related to data creation and update.
- `blacklist` Fields blacklist, used in actions related to data creation and update.
### filter
Data filtering.
```bash
# simple
GET /api/posts?filter[status]=publish
# json string format is recommended, which requires encodeURIComponent encoding
GET /api/posts?filter={"status":"published"}
# filter operators
GET /api/posts?filter[status.$eq]=publish
GET /api/posts?filter={"status.$eq":"published"}
# $and
GET /api/posts?filter={"$and": [{"status.$eq":"published"}, {"title.$includes":"a"}]}
# $or
GET /api/posts?filter={"$or": [{"status.$eq":"pending"}, {"status.$eq":"draft"}]}
# association field
GET /api/posts?filter[user.email.$includes]=gmail
GET /api/posts?filter={"user.email.$includes":"gmail"}
```
[Click here for more information about filter operators](http-api/filter-operators)
### filterByTk
Filter by tk field. In the default settings:
- collection resource: tk is the primary key of the data table.
- association resource: tk is the targetKey field of the association.
```bash
GET /api/posts:get?filterByTk=1&fields=name,title&appends=tags
```
### sort
Sorting. To sort in the descending order, put `-` in front of the field.
```bash
# Sort createAt field in the ascending order
GET /api/posts:get?sort=createdAt
# Sort createAt field in the descending order
GET /api/posts:get?sort=-createdAt
# Sort multiple fields jointly, createAt field descending, title A-Z ascending
GET /api/posts:get?sort=-createdAt,title
```
### fields
Data to output.
```bash
GET /api/posts:list?fields=name,title
Response 200 (application/json)
{
"data": [
{
"name": "",
"title": ""
}
],
"meta": {}
}
```
### appends
Fields of additional relationship.
### except
Exclude some fields (not to output), used in actions related to query.
### whitelist
Whitelist.
```bash
POST /api/posts:create?whitelist=title
{
"title": "My first post",
"date": "2022-05-19" # The date field will be filtered out and not be written to the database
}
```
### blacklist
Blacklist.
```bash
POST /api/posts:create?blacklist=date
# The date field will be filtered out and not be written to the database
{
"title": "My first post"
}
```
## Request Response
Format of the response:
```ts
type ResponseResult = {
data?: any; // Main data
meta?: any; // Additional Data
errors?: ResponseError[]; // Errors
};
type ResponseError = {
code?: string;
message: string;
};
```
### Example
View list:
```bash
GET /api/posts:list
Response 200 (application/json)
{
data: [
{
id: 1
}
],
meta: {
count: 1
page: 1,
pageSize: 1,
totalPage: 1
},
}
```
View details:
```bash
GET /api/posts:get/1
Response 200 (application/json)
{
data: {
id: 1
}
}
```
Error:
```bash
POST /api/posts:create
Response 400 (application/json)
{
errors: [
{
message: 'name must be required',
},
],
}
```

View File

@ -1,180 +0,0 @@
# REST API
HTTP API of NocoBase is a superset of REST API, and the standard CRUD API also supports the RESTful style.
## Collection Resources
### Create Collection
HTTP API
```bash
POST /api/<collection>:create
{} # JSON body
```
REST API
```bash
POST /api/<collection>
{} # JSON body
```
### View Collection List
HTTP API
```bash
GET /api/<collection>:list
```
REST API
```bash
GET /api/<collection>
```
### View Collection Details
HTTP API
```bash
GET /api/<collection>:get?filterByTk=<collectionIndex>
GET /api/<collection>:get/<collectionIndex>
```
REST API
```bash
GET /api/<collection>/<collectionIndex>
```
### Update Collection
HTTP API
```bash
POST /api/<collection>:update?filterByTk=<collectionIndex>
{} # JSON body
# Or
POST /api/<collection>:update/<collectionIndex>
{} # JSON body
```
REST API
```bash
PUT /api/<collection>/<collectionIndex>
{} # JSON body
```
### Delete Collection
HTTP API
```bash
POST /api/<collection>:destroy?filterByTk=<collectionIndex>
# Or
POST /api/<collection>:destroy/<collectionIndex>
```
REST API
```bash
DELETE /api/<collection>/<collectionIndex>
```
## Association Resources
### Create Association
HTTP API
```bash
POST /api/<collection>/<collectionIndex>/<association>:create
{} # JSON body
```
REST API
```bash
POST /api/<collection>/<collectionIndex>/<association>
{} # JSON body
```
### View Association List
HTTP API
```bash
GET /api/<collection>/<collectionIndex>/<association>:list
```
REST API
```bash
GET /api/<collection>/<collectionIndex>/<association>
```
### View Association Details
HTTP API
```bash
GET /api/<collection>/<collectionIndex>/<association>:get?filterByTk=<associationIndex>
# Or
GET /api/<collection>/<collectionIndex>/<association>:get/<associationIndex>
```
REST API
```bash
GET /api/<collection>/<collectionIndex>/<association>:get/<associationIndex>
```
### Update Association
HTTP API
```bash
POST /api/<collection>/<collectionIndex>/<association>:update?filterByTk=<associationIndex>
{} # JSON body
# Or
POST /api/<collection>/<collectionIndex>/<association>:update/<associationIndex>
{} # JSON body
```
REST API
```bash
PUT /api/<collection>/<collectionIndex>/<association>:update/<associationIndex>
{} # JSON
```
### Delete Association
HTTP API
```bash
POST /api/<collection>/<collectionIndex>/<association>:destroy?filterByTk=<associationIndex>
# Or
POST /api/<collection>/<collectionIndex>/<association>:destroy/<associationIndex>
```
REST API
```bash
DELETE /api/<collection>/<collectionIndex>/<association>/<associationIndex>
```

View File

@ -1,12 +0,0 @@
# Overview
| Modules | Package Name | Description |
|-----------------------------------| --------------------- | ------------------- |
| [Server](/api/server/application) | `@nocobase/server` | Server-side application |
| [Database](/api/database) | `@nocobase/database` | Database access layer |
| [Resourcer](/api/resourcer) | `@nocobase/resourcer` | Resource and route mapping |
| [ACL](/api/acl) | `@nocobase/acl` | Access Control List |
| [Client](/api/client/application) | `@nocobase/client` | Client-side application |
| [CLI](/api/cli) | `@nocobase/cli` | NocoBase command line tools |
| [SDK](/api/sdk) | `@nocobase/sdk` | NocoBase SDK |
| [Actions](/api/actions) | `@nocobase/actions` | Built-in common resource actions |

View File

@ -1,148 +0,0 @@
# Action
Action is the description of the operation process on resource, including database processing and so on. It is like the service layer in other frameworks, and the most simplified implementation can be a Koa middleware function. In the resourcer, common action functions defined for particular resources are wrapped into instances of the type Action, and when the request matches the action of the corresponding resource, the corresponding action is executed.
## Constructor
Instead of being instantiated directly, the Action is usually instantiated automatically by the resourcer by calling the static method `toInstanceMap()` of `Action`.
### `constructor(options: ActionOptions)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `handler` | `Function` | - | Handler function |
| `middlewares?` | `Middleware \| Middleware[]` | - | Middlewares for the action |
| `values?` | `Object` | - | Default action data |
| `fields?` | `string[]` | - | Default list of targeted fields |
| `appends?` | `string[]` | - | Default list of associated fields to append |
| `except?` | `string[]` | - | Default list of fields to exclude |
| `filter` | `FilterOptions` | - | Default filtering options |
| `sort` | `string[]` | - | Default sorting options |
| `page` | `number` | - | Default page number |
| `pageSize` | `number` | - | Default page size |
| `maxPageSize` | `number` | - | Default maximum page size |
## Instance Members
### `actionName`
Name of the action that corresponds to when it is instantiated. It is parsed and fetched from the request at instantiation.
### `resourceName`
Name of the resource that corresponds to when the action is instantiated. It is parsed and fetched from the request at instantiation.
### `resourceOf`
Value of the primary key of the relational resource that corresponds to when the action is instantiated. It is parsed and fetched from the request at instantiation.
### `readonly middlewares`
List of middlewares targeting the action.
### `params`
Action parameters. It contains all relevant parameters for the corresponding action, which are initialized at instantiation according to the defined action parameters. Later when parameters passed from the front-end are parsed in requests, the corresponding parameters are merged according to the merge strategy. Similar merging process is done if there is other middleware processing. When it comes to the hander, the `params` are the final parameters that have been merged for several times.
The merging process of parameters provides scalability for action processing, and the parameters can be pre-parsed and processed according to business requirements by means of custom middleware. For example, parameter validation for form submission can be implemented in this part.
Refer to [/api/actions] for the pre-defined parameters of different actions.
The parameters also contain a description of the request resource route:
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `resourceName` | `string` | - | Name of the resource |
| `resourceIndex` | `string \| number` | - | Value of the primary key of the resource |
| `associatedName` | `string` | - | Name of the associated resource it belongs to |
| `associatedIndex` | `string \| number` | - | Value of the primary key of the associated resource it belongs to |
| `associated` | `Object` | - | Instance of the associated resource it belongs to |
| `actionName` | `string` | - | Name of the action |
**Example**
```ts
app.resourcer.define('books', {
actions: {
publish(ctx, next) {
ctx.body = ctx.action.params.values;
// {
// id: 1234567890
// publishedAt: '2019-01-01',
// }
}
},
middlewares: [
async (ctx, next) => {
ctx.action.mergeParams({
values: {
id: Math.random().toString(36).substr(2, 10),
publishedAt: new Date(),
}
});
await next();
}
]
});
```
## Instance Methods
### `mergeParams()`
Merge additional parameters to the current set of parameters according to different strategies.
**Signature**
* `mergeParams(params: ActionParams, strategies: MergeStrategies = {})`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `params` | `ActionParams` | - | Additional set of parameters |
| `strategies` | `MergeStrategies` | - | Merge strategies for each parameter |
The default merge strategy for built-in actions is as follows:
| Name | Type | Default | Merge Strategy |Description |
| --- | --- | --- | --- | --- |
| `filterByTk` | `number \| string` | - | SQL `and` | Get value of the primary key |
| `filter` | `FilterOptions` | - | SQL `and` | Get filtering options |
| `fields` | `string[]` | - | Take the union | List of fields |
| `appends` | `string[]` | `[]` | Take the union | List of associated fields to append |
| `except` | `string[]` | `[]` | Take the union | List of associated fields to exclude |
| `whitelist` | `string[]` | `[]` | Take the intersection | Whitelist of fields that can be handled |
| `blacklist` | `string[]` | `[]` | Take the union | Blacklist of fields that can be handled |
| `sort` | `string[]` | - | SQL `order by` | Get the sorting options |
| `page` | `number` | - | Override | Page number |
| `pageSize` | `number` | - | Override | Page size |
| `values` | `Object` | - | Deep merge | Operation of the submitted data |
**Example**
```ts
ctx.action.mergeParams({
filter: {
name: 'foo',
},
fields: ['id', 'name'],
except: ['name'],
sort: ['id'],
page: 1,
pageSize: 10,
values: {
name: 'foo',
},
}, {
filter: 'and',
fields: 'union',
except: 'union',
sort: 'overwrite',
page: 'overwrite',
pageSize: 'overwrite',
values: 'deepMerge',
});
```

View File

@ -1,285 +0,0 @@
# Resourcer
## Overview
The interfaces in NocoBase follow a resource-oriented design pattern. Resourcer is mainly used to manage the resources and routes of API.
```javascript
const Koa = require('koa');
const { Resourcer } = require('@nocobase/resourcer');
const resourcer = new Resourcer();
// Define a resource interface
resourcer.define({
name: 'users',
actions: {
async list(ctx) {
ctx.body = [
{
name: "u1",
age: 18
},
{
name: "u2",
age: 20
}
]
}
},
});
const app = new Koa();
// Use the resourcer in koa instance
app.use(
resourcer.middleware({
prefix: '/api', // Route prefix of the resourcer
}),
);
app.listen(3000);
```
Once the service is started, make request using `curl`:
```bash
>$ curl localhost:3000/api/users
[{"name":"u1","age":18},{"name":"u2","age":20}]
```
More instructions of resourcer can be found in [Resources and Actions](/development/guide/resources-actions). Resourcer is built into [NocoBase Application](/api/server/application#resourcer), you can access it through `app.resourcer`.
## Constructor
To create resourcer instances.
**Signature**
* `constructor(options: ResourcerOptions)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `prefix` | `string` | - | Route prefix |
| `accessors` | `Object` | _The following values of members_ | Name identifier of the default operation method |
| `accessors.list` | `string` | `'list'` | Name identifier of the list operation method |
| `accessors.get` | `string` | `'get'` | Name identifier of the get operation method |
| `accessors.create` | `string` | `'create'` | Name identifier of the create operation method |
| `accessors.update` | `string` | `'update'` | Name identifier of the update operation method |
| `accessors.delete` | `string` | `'destroy'` | Name identifier of the delete operation method |
| `accessors.add` | `string` | `'add'` | Name identifier of the add association operation method |
| `accessors.remove` | `string` | `'remove'` | Name identifier of the remove association operation method |
| `accessors.set` | `string` | `'set'` | Name identifier of the global set association operation method |
**Example**
Pass in through the `resourcer` option when creating app:
```ts
const app = new Application({
// Correspond to the configuration item of the default resourcer instance
resourcer: {
prefix: process.env.API_BASE_PATH
}
});
```
## Instance Methods
### `define()`
Define and register a resource object with the resourcer. Usually used instead of the constructor of the `Resource` class.
**Signature**
* `define(options: ResourceOptions): Resource`
**Parameter**
Refer to [Resource Constructor](/api/server/resourcer/resource#constructor) for details.
**Example**
```ts
app.resourcer.define({
name: 'books',
actions: {
// Extended action
publish(ctx, next) {
ctx.body = 'ok';
}
}
});
```
### `isDefined()`
Check whether the resource with the corresponding name has been registered.
**Signature**
* `isDefined(name: string): boolean`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the resource |
**Example**
```ts
app.resourcer.isDefined('books'); // true
```
### `registerAction()`
Register an action with the resourcer. The action is accessible to a specified resource, or all resources if no resource name is specified.
**Signature**
* `registerAction(name: string, handler: HandlerType): void`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the action |
| `handler` | `HandlerType` | - | Handler of the action |
A value of `name` starting with `<resourceName>:` means that the action is only accessible to `<resourceName>` rescource, otherwise it is considered as a global action.
**Example**
```ts
// All resources can take the upload action after registration
app.resourcer.registerAction('upload', async (ctx, next) => {
ctx.body = 'ok';
});
// Register the upload action only for attachments resource
app.resourcer.registerAction('attachments:upload', async (ctx, next) => {
ctx.body = 'ok';
});
```
### `registerActions()`
Register a set of actions with the resourcer.
**Signature**
* `registerActions(actions: { [name: string]: HandlerType }): void`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `actions` | `{ [name: string]: HandlerType }` | - | Set of actions |
**Example**
```ts
app.resourcer.registerActions({
upload: async (ctx, next) => {
ctx.body = 'ok';
},
'attachments:upload': async (ctx, next) => {
ctx.body = 'ok';
}
});
```
### `getResource()`
Get the resource object with the corresponding name.
**Signature**
* `getResource(name: string): Resource`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the resource |
**Example**
```ts
app.resourcer.getResource('books');
```
### `getAction()`
Get the action handler function with the corresponding name.
**Signature**
* `getAction(name: string): HandlerType`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the action |
A value of `name` starting with `<resourceName>:` means that the action is only accessible to `<resourceName>` rescource, otherwise it is considered as a global action.
**Example**
```ts
app.resourcer.getAction('upload');
app.resourcer.getAction('attachments:upload');
```
### `use()`
Register a middleware in the form of Koa; the middleware forms a queue which is executed before the action handlers of all resources.
**Signature**
* `use(middleware: Middleware | Middleware[]): void`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `middleware` | `Middleware \| Middleware[]` | - | Middleware |
**Example**
```ts
app.resourcer.use(async (ctx, next) => {
console.log(ctx.req.url);
await next();
});
```
### `middleware()`
Generate a Koa-compatible middleware for injecting routing processing of resources into the application.
**Signature**
* `middleware(options: KoaMiddlewareOptions): KoaMiddleware`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.prefix?` | `string` | `''` | Route prefix |
| `options.accessors?` | `Object` | `{}` | Name mapping for common methods, with the same parameter structure as `accessors` of the constructor |
**Example**
```ts
const koa = new Koa();
const resourcer = new Resourcer();
// Generate Koa-compatible middleware
koa.use(resourcer.middleware());
```

View File

@ -1,168 +0,0 @@
# Middleware
It is similar to the middleware of Koa, but with more enhanced features for easy extensions.
The defined middleware can be inserted for use in multiple places, such as the resourcer, and it is up to the developer for when to invoke it.
## Constructor
**Signature**
* `constructor(options: Function)`
* `constructor(options: MiddlewareOptions)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options` | `Function` | - | Handler function of middlware |
| `options` | `MiddlewareOptions ` | - | Configuration items of middlware |
| `options.only` | `string[]` | - | Only the specified actions are allowed |
| `options.except` | `string[]` | - | The specified actions are excluded |
| `options.handler` | `Function` | - | Handler function |
**Example**
Simple definition:
```ts
const middleware = new Middleware((ctx, next) => {
await next();
});
```
Definition with relevant parameters:
```ts
const middleware = new Middleware({
only: ['create', 'update'],
async handler(ctx, next) {
await next();
},
});
```
## Instance Methods
### `getHandler()`
Get the orchestrated handler functions.
**Example**
The following middleware will output `1` and then `2` when requested.
```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()`
Add a middleware function to the current middleware. Used to provide extension points for the middleware. See `getHandler()` for the examples.
**Signature**
* `use(middleware: Function)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `middleware` | `Function` | - | Handler function of the middleware |
### `disuse()`
Remove the middleware functions that have been added to the current middleware.
**Signature**
* `disuse(middleware: Function)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `middleware` | `Function` | - | Handler function of the middleware |
**Example**
The following example will only output `1` when requested, the output of `2` in fn1 will not be executed.
```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()`
Check whether the current middleware is to be invoked for a specific action, it is usually handled by the resourcer internally.
**Signature**
* `canAccess(name: string): boolean`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the action |
## Other Exports
### `branch()`
Create a branch middleware for branching in the middleware.
**Signature**
* `branch(map: { [key: string]: Function }, reducer: Function, options): Function`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `map` | `{ [key: string]: Function }` | - | Mapping table of the branch handler function, key names are given by subsequent calculation functions when called |
| `reducer` | `(ctx) => string` | - | Calculation function, it is used to calculate the key name of the branch based on the context |
| `options?` | `Object` | - | Configuration items of the branch |
| `options.keyNotFound?` | `Function` | `ctx.throw(404)` | Handler function when key name is not found |
| `options.handlerNotSet?` | `Function` | `ctx.throw(404)` | The function when no handler function is defined |
**Example**
When authenticating user, determine what to do next according to the value of the `authenticator` parameter in the query section of the request URL.
```ts
app.resourcer.use(branch({
'password': async (ctx, next) => {
// ...
},
'sms': async (ctx, next) => {
// ...
},
}, (ctx) => {
return ctx.action.params.authenticator ?? 'password';
}));
```

View File

@ -1,114 +0,0 @@
# Resource
Resource is used to define resource instance. Resource instances managed by resourcer can be accessed through HTTP requests.
## Constructor
To create resource instance. Normally it is not used directly, but replaced by the call of the `define()` interface of resourcer.
**Signature**
* `constructor(options: ResourceOptions, resourcer: Resourcer)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.name` | `string` | - | Name of the resource, corresponding to the resource address in the route of the URL |
| `options.type` | `string` | `'single'` | Type of the resource, options are `'single'`, `'hasOne'`, `'hasMany'`, `'belongsTo'`, `'belongsToMany'` |
| `options.actions` | `Object` | - | List of actions that can be taken on the resource, see the example for details |
| `options.middlewares` | `MiddlewareType \| MiddlewareType[]` | - | List of middlewares for any operational access to the resource that is definingsee the example for details |
| `options.only` | `ActionName[]` | `[]` | Whitelist for global actions, only actions contained in the array (if `length > 0`) can be accessed |
| `options.except` | `ActionName[]` | `[]` | Blacklist for global actions, all actions except those contained in the array (if `length > 0`) can be accessed |
| `resourcer` | `Resourcer` | - | The resourcer instance |
**Example**
```ts
app.resourcer.define({
name: 'books',
actions: {
// Extended action
publish(ctx, next) {
ctx.body = 'ok';
}
},
middleware: [
// Extended middleware
async (ctx, next) => {
await next();
}
]
});
```
## Instance Members
### `options`
Configuration items for the current resource.
### `resourcer`
The resourcer instance to which the resource belongs.
### `middlewares`
The registered middlewares.
### `actions`
The registered mapping table of actions.
### `except`
Actions that are excluded.
## Instance Methods
### `getName()`
Get the name of the current resource.
**Signature**
* `getName(): string`
**Example**
```ts
const resource = app.resourcer.define({
name: 'books'
});
resource.getName(); // 'books'
```
### `getAction()`
Get action with the corresponding name.
**Signature**
* `getAction(name: string): Action`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string` | - | Name of the action |
**Example**
```ts
const resource = app.resourcer.define({
name: 'books',
actions: {
publish(ctx, next) {
ctx.body = 'ok';
}
}
});
resource.getAction('publish'); // [Function: publish]
```

View File

@ -1,281 +0,0 @@
# @nocobase/sdk
## APIClient
```ts
class APIClient {
// axios instance
axios: AxiosInstance;
// Constructor
constructor(instance?: AxiosInstance | AxiosRequestConfig);
// Request from client, support AxiosRequestConfig and ResourceActionOptions
request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D> | ResourceActionOptions): Promise<R>;
// Get resource
resource<R = IResource>(name: string, of?: any): R;
}
```
Instance initialization:
```ts
import axios from 'axios';
import { APIClient } from '@nocobase/sdk';
// Provide AxiosRequestConfig configuration parameters
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
// Provide AxiosInstance
const instance = axios.create({
baseURL: 'https://localhost:8000/api',
});
const api = new APIClient(instance);
```
## Mock
```ts
import { APIClient } from '@nocobase/sdk';
import MockAdapter from 'axios-mock-adapter';
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
});
const mock = new MockAdapter(api.axios);
mock.onGet('users:get').reply(200, {
data: { id: 1, name: 'John Smith' },
});
await api.request({ url: 'users:get' });
```
## Storage
APIClient uses localStorage by default, but you can also customize the Storage, for example:
```ts
import { Storage } from '@nocobase/sdk';
class MemoryStorage extends Storage {
items = new Map();
clear() {
this.items.clear();
}
getItem(key: string) {
return this.items.get(key);
}
setItem(key: string, value: string) {
return this.items.set(key, value);
}
removeItem(key: string) {
return this.items.delete(key);
}
}
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
storageClass: CustomStorage,
});
```
## Auth
```ts
// Sign in and record token
api.auth.signIn({ email, password });
// Sign out and remove token
api.auth.signOut();
// Set token
api.auth.setToken('123');
// Set role (When multiple roles are needed)
api.auth.setRole('admin');
// Set locale (When multiple languages are needed)
api.auth.setLocale('zh-CN');
```
Auth customization:
```ts
import { Auth } from '@nocobase/sdk';
class CustomAuth extends Auth {
}
const api = new APIClient({
baseURL: 'https://localhost:8000/api',
authClass: CustomAuth,
});
```
## Request
```ts
// url
await api.request({
url: 'users:list',
// request params
params: {
filter: {
'email.$includes': 'noco',
},
},
// request body
data,
});
// resource & action
await api.request({
resource: 'users',
action: 'list',
// action params
params: {
filter: {
'email.$includes': 'noco',
},
page: 1,
},
});
```
## Resource action
```ts
await api.resource('collection')[action]();
await api.resource('collection.association', collectionId)[action]();
```
## Action API
```ts
await api.resource('collection').create();
await api.resource('collection').get();
await api.resource('collection').list();
await api.resource('collection').update();
await api.resource('collection').destroy();
await api.resource('collection.association', collectionId).create();
await api.resource('collection.association', collectionId).get();
await api.resource('collection.association', collectionId).list();
await api.resource('collection.association', collectionId).update();
await api.resource('collection.association', collectionId).destroy();
```
### `get`
```ts
interface Resource {
get: (options?: GetActionOptions) => Promise<any>;
}
interface GetActionOptions {
filter?: any;
filterByTk?: any;
fields?: string || string[];
appends?: string || string[];
expect?: string || string[];
sort?: string[];
}
```
### `list`
```ts
interface Resource {
list: (options?: ListActionOptions) => Promise<any>;
}
interface ListActionOptions {
filter?: any;
filterByTk?: any;
fields?: string || string[];
appends?: string || string[];
expect?: string || string[];
sort?: string[];
page?: number;
pageSize?: number;
paginate?: boolean;
}
```
### `create`
```ts
interface Resource {
create: (options?: CreateActionOptions) => Promise<any>;
}
interface CreateActionOptions {
whitelist?: string[];
blacklist?: string[];
values?: {[key: sting]: any};
}
```
### `update`
```ts
interface Resource {
update: (options?: UpdateActionOptions) => Promise<any>;
}
interface UpdateActionOptions {
filter?: any;
filterByTk?: any;
whitelist?: string[];
blacklist?: string[];
values?: {[key: sting]: any};
}
```
### `destroy`
```ts
interface Resource {
destroy: (options?: DestroyActionOptions) => Promise<any>;
}
interface DestroyActionOptions {
filter?: any;
filterByTk?: any;
}
```
### `move`
```ts
interface Resource {
move: (options?: MoveActionOptions) => Promise<any>;
}
interface MoveActionOptions {
sourceId: any;
targetId?: any;
/** @default 'sort' */
sortField?: any;
targetScope?: {[key: string]: any};
sticky?: boolean;
method?: 'insertAfter' | 'prepend';
}
```
### `<custom>`
```ts
interface AttachmentResource {
}
interface UploadActionOptions {
}
api.resource<AttachmentResource>('attachments').upload();
api.resource('attachments').upload<UploadActionOptions>();
```

View File

@ -1 +0,0 @@
# AppManager

View File

@ -1,303 +0,0 @@
# Application
## Overview
### Web Service
NocoBase Application is a web framework implemented based on [Koa](https://koajs.com/), compatible with Koa API.
```javascript
// index.js
const { Application } = require('@nocobase/server');
// Create App instance and configure the database
const app = new Application({
database: {
dialect: 'sqlite',
storage: ':memory:',
}
});
// Register middleware, response to requests
app.use(async ctx => {
ctx.body = 'Hello World';
});
// Run in the CLI mode
app.runAsCLI();
```
After running `node index.js start` in CLI to start service, use `curl` to request service.
```bash
$> curl localhost:3000
Hello World
```
### CLI Tool
NocoBase Application has a built-in `cli commander`, which can be run as CLI tool.
```javascript
// cmd.js
const {Application} = require('@nocobase/server');
const app = new Application({
database: {
dialect: 'sqlite',
storage: ':memory:',
}
});
app.cli.command('hello').action(async () => {
console.log("hello world")
});
app.runAsCLI()
```
Run in CLI:
```bash
$> node cmd.js hello
hello world
```
### Inject Plugin
NocoBase Application is designed as a highly extensible framework, plugins can be written and injected to the applicationto to extend its functionality. For example, the above-mentioned web service can be replaced with a plugin.
```javascript
const { Application, Plugin } = require('@nocobase/server');
// Write plugin by inheriting the Plugin class
class HelloWordPlugin extends Plugin {
load() {
this.app.use(async (ctx, next) => {
ctx.body = "Hello World";
})
}
}
const app = new Application({
database: {
dialect: 'sqlite',
storage: ':memory:',
}
});
// Inject plugin
app.plugin(HelloWordPlugin, { name: 'hello-world-plugin'} );
app.runAsCLI()
```
### More Examples
Please refer to the detailed guides of [plugin development](./plugin.md). Read more [examples](https://github.com/nocobase/nocobase/blob/main/examples/index.md) of the Application class.
## Lifecycle
Application has three lifecycle stages depends on the running mode.
### Install
Use the `install` command in `cli` to invoke the installation. Generally, if needs to write new tables or data to the database before using the plugin, you have to do it during installation. Installation is also required when using NocoBase for the first time.
* Call the `load` method to load registered plugins.
* Trigger the `beforeInstall` event.
* Call the `db.sync` method to synchronize database.
* Call the `pm.install` method to execute the `install` methods of registered plugins.
* Write the version of `nocobase`.
* Trigger the `afterInstall` event.
* Call the `stop` method to end installation.
### Start
Use the `start` command in `cli` to start NocoBase Web service.
* Call the `load` method to load registered plugins.
* Call the `start` medthod:
* Trigger the `beforeStart` event
* Start port listening
* Trigger the `afterStart` event
### Upgrade
Use the `upgrade` command in `cli` to upgrade NocoBase Web service when needed.
* Call the `load` method to load registered plugins.
* Trigger the `beforeUpgrade` event.
* Call the `db.migrator.up` method to migrate database.
* Call the `db.sync` method to synchronize database.
* Call the `version.update` method to update the version of `nocobase`.
* Trigger the `afterUpgrade` event.
* Call the `stop` medthod to end upgrade.
## Constructor
### `constructor()`
Create an application instance.
**Signature**
* `constructor(options: ApplicationOptions)`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.database` | `IDatabaseOptions` or `Database` | `{}` | Database configuration |
| `options.resourcer` | `ResourcerOptions` | `{}` | Resource route configuration |
| `options.logger` | `AppLoggerOptions` | `{}` | Log |
| `options.cors` | [`CorsOptions`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/koa__cors/index.d.ts#L24) | `{}` | Cross-domain configuration, refer to [@koa/cors](https://npmjs.com/package/@koa/cors) |
| `options.dataWrapping` | `boolean` | `true` | Whether or not to wrap the response data, `true` will wrap the usual `ctx.body` into a `{ data, meta }` structure |
| `options.registerActions` | `boolean` | `true` | Whether or not to register the default [actions](#) |
| `options.i18n` | `I18nOptions` | `{}` | Internationalization configuration, refer to [i18next](https://www.i18next.com/overview/api) |
| `options.plugins` | `PluginConfiguration[]` | `[]` | Configuration of the plugins enabled by default |
Type
```ts
interface ApplicationOptions {
}
```
## Instance Members
### `cli`
CLI tool instance, refer to the npm package [Commander](https://www.npmjs.com/package/commander)。
### `db`
Database instance, refer to [Database](/api/database) for the related API.
### `resourcer`
Resource route management instance created automatically during app initialization, refer to [Resourcer](/api/resourcer) for the related API.
### `acl`
ACL instance, refer to [ACL](/api/acl) for the related API.
### `logger`
Winston instance, refer to [Winston](https://github.com/winstonjs/winston#table-of-contents) for the related API.
### `i18n`
I18next instance, refer to [I18next](https://www.i18next.com/overview/api) for the related API.
### `pm`
Plugin manager instance, refer to [PluginManager](./plugin-manager) for the related API.
### `version`
App version instance, refer to [ApplicationVersion](./application-version) for the related API.
### `middleware`
Built-in middleware includes:
- logger
- i18next
- bodyParser
- cors
- dataWrapping
- db2resource
- restApiMiddleware
### `context`
Context inherited from koa, accessible via `app.context`, is used to inject context-accessible content to each request. Refer to [Koa Context](https://koajs.com/#app-context).
NocoBase injects the following members to context by default, which can be used directly in the request handler function:
| Variable Name | Type | Description |
| --- | --- | --- |
| `ctx.app` | `Application` | Application instance |
| `ctx.db` | `Database` | Database instance |
| `ctx.resourcer` | `Resourcer` | Resource route manager instance |
| `ctx.action` | `Action` | Resource action related object instance |
| `ctx.logger` | `Winston` | Log instance |
| `ctx.i18n` | `I18n` | Internationlization instance |
| `ctx.t` | `i18n.t` | Shortcut of internationalized translation function |
| `ctx.getBearerToken()` | `Function` | Get the bearer token in the header of request |
## Instance Methods
### `use()`
Register middleware, compatible with all [Koa plugins](https://www.npmjs.com/search?q=koa).
### `on()`
Subscribe to application-level events, mainly are related to lifecycle. It is equivalent to `eventEmitter.on()`. Refer to [events](#events) for all subscribable events.
### `command()`
Customize command.
### `findCommand()`
Find defined command.
### `runAsCLI()`
Run as CLI.
### `load()`
Load application configuration.
**Signature**
* `async load(): Promise<void>`
### `reload()`
Reload application configuration.
### `install()`
Initialize the installation of the application, meanwhile, install the plugin.
### `upgrade()`
Upgrade application, meanwhile, upgrade plugin.
### `start()`
Start application, listening will also be started if the listening port is configured, then the application can accept HTTP requests.
**Signature**
* `async start(options: StartOptions): Promise<void>`
**Parameter**
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.listen?` | `ListenOptions` | `{}` | HTTP Listening parameters object |
| `options.listen.port?` | `number` | 13000 | Port |
| `options.listen.host?` | `string` | `'localhost'` | Domain name |
### `stop()`
Stop application. This method will deconnect database, close HTTP port, but will not delete data.
### `destroy()`
Delete application. This methos will delete the corresponding database of application.
## Events
### `'beforeLoad'` / `'afterLoad'`
### `'beforeInstall'` / `'afterInstall'`
### `'beforeUpgrade'` / `'afterUpgrade'`
### `'beforeStart'` / `'afterStart'`
### `'beforeStop'` / `'afterStop'`
### `'beforeDestroy'` / `'afterDestroy'`

View File

@ -1 +0,0 @@
# I18n

View File

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

View File

@ -1,47 +0,0 @@
# Plugin
## Overview
Plugins in NocoBase are in the form of `Class`. Custom plugins need to inherit the `Plugin` class.
```typescript
import { Plugin } from '@nocobase/server';
class MyPlugin extends Plugin {
// ...
}
app.plugin(MyPlugin, { name: 'my-plugin' });
```
## Plugin Lifecycle
Each plugin contains lifecycle methods, you can override these methods in order to execute them at certain stages during runtime. Lifecycle methods will be called by `Application` at certain stages, refer to [`Application` LifeCycle](./application.md).
### `beforeLoad()`
To implement the logic before plugin is loaded, such as event or class registration. The core interface can be accessed here, while other plugins are not available.
### `load()`
To implement the logic to load plugin, configurations and so on. Other plugin instances can be called in `load`, but not in `beforeLoad`.
### `install()`
To implement the logic to install plugin, such as data initialization.
### `afterAdd()`
To implement the logic after the add/addStatic of plugin.
### `afterEnable()`
To implement the logic after plugin is enabled.
### `afterDisable()`
To implement the logic after plugin is disabled.
### `remove()`
To implement the logic to remove plugin.

View File

@ -1 +0,0 @@
# Components

View File

@ -1,64 +0,0 @@
# App directory structure
Either [Git source](/welcome/getting-started/installation/git-clone) or [create-nocobase-app](/welcome/getting-started/installation/create-nocobase-app), the directory structure of the created NocoBase application is the same, with the following structure.
```bash
├── my-nocobase-app
├── packages # NocoBase uses the Monorepo approach to manage code, dividing different modules into different packages
├── app
├── client # client-side module
├── server # server-side modules
├─ plugins # plugin directory
├── storage # for database files, attachments, cache, etc.
├─ db
├── .env # environment variables
├── .buildrc.ts # package configuration for packages, supports cjs, esm and umd packages.
├── jest.config.js
├── jest.setup.ts
├── lerna.json
├── package.json
├── tsconfig.jest.json
├─ tsconfig.json
├── tsconfig.server.json
```
## Packages directory
```bash
├─ packages
├─ app
├── client
├─ public
├─ src
├─ pages
├─ index.tsx
├─ .umirc.ts
├─ package.json
├─ server
├─ src
├─ config
├─ index.ts
├─ package.json
├─ /plugins
├─ my-plugin
├─ src
├─ package.json
```
NocoBase uses the Monorepo approach to manage code, dividing different modules into different packages.
- `app/client` is the client module of the application, built on [umi](https://umijs.org/zh-CN).
- `app/server` is the server-side module of the application.
- The `plugins/*` directory can hold various plugins.
## storages directory
Store database files, attachments, cache, etc.
## .env files
Environment variables.
## .buildrc.ts file
Package configuration for packages, supporting cjs, esm and umd formats.

View File

@ -1,140 +0,0 @@
# Internationalization
Client-side internationalization for multiple languages is implemented based on the npm package [react-i18next](https://npmjs.com/package/react-i18next), which provides a wrapper for the `<I18nextProvider>` component at the top level of the application, allowing the relevant methods to be used directly at any location.
Adding language packages:
```tsx | pure
import { i18n } from '@nocobase/client';
i18n.addResources('zh-CN', 'test', {
Hello: '你好',
World: '世界',
});
```
Note: Here the second parameter filled in `'test'` is the language namespace, usually the plugin itself defines the language resources should create a specific namespace according to their own plugin package name, in order to avoid conflicts with other language resources. The default namespace in NocoBase is `'client'` and most common and basic language translations are placed in this namespace. When the required language is not provided, it can be defined by extension in the plugin's own namespace.
To call the translation function in the component:
```tsx | pure
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function MyComponent() {
// Use the previously defined namespace
const { t } = useTranslation('test');
return (
<div>
<p>{t('World')}</p>
</div>
);
}
```
The template method `'{{t(<languageKey>)}}'` can be used directly in the SchemaComponent component, and the translation functions in the template will automatically be executed.
```tsx | pure
import React from 'react';
import { SchemaComponent } from '@nocobase/client';
export default function MySchemaComponent() {
return (
<SchemaComponent
schema={{
type: 'string',
'x-component': 'Input',
'x-component-props': {
value: '{{t("Hello", { ns: "test" })}}'
},
}}
/>
);
}
```
In some special cases where it is also necessary to define multilingualism as a template, the NocoBase built-in `compile()` method can be used to compile to multilingual results.
```tsx | pure
import React from 'react';
import { useCompile } from '@nocobase/client';
const title = '{{t("Hello", { ns: "test" })}}';
export default function MyComponent() {
const { compile } = useCompile();
return (
<div>{compile(title)}</div>
);
}
```
## Suggested configuration
With English text as the key and translation as the value, the benefit of this, even if multiple languages are missing, it will be displayed in English and will not cause reading barriers, e.g.
```ts
i18n.addResources('zh-CN', 'my-plugin', {
'Show dialog': '显示对话框',
'Hide dialog': '隐藏对话框'
});
```
To make it easier to manage multilingual files, it is recommended to create a `locale` folder in the plugin and place all the corresponding language files in it for easy management.
```bash
|- /my-plugin
|- /src
|- /client
|- locale # Multilingual folder
|- zh-CN.ts
|- en-US.ts
```
## Example
### Client-side components with multiple languages
For example, the order status component, with different text displays depending on the value.
```tsx | pure
import React from 'react';
import { Select } from 'antd';
import { i18n } from '@nocobase/client';
import { useTranslation } from 'react-i18next';
i18n.addResources('zh-CN', 'sample-shop-i18n', {
Pending: '已下单',
Paid: '已支付',
Delivered: '已发货',
Received: '已签收'
});
const ORDER_STATUS_LIST = [
{ value: -1, label: 'Canceled (untranslated)' },
{ value: 0, label: 'Pending' },
{ value: 1, label: 'Paid' },
{ value: 2, label: 'Delivered' },
{ value: 3, label: 'Received' },
]
function OrderStatusSelect() {
const { t } = useTranslation('sample-shop-i18n');
return (
<Select style={{ minWidth: '8em' }}>
{ORDER_STATUS_LIST.map(item => (
<Select.Option value={item.value}>{t(item.label)}</Select.Option>
))}
</Select>
);
}
export default function () {
return (
<OrderStatusSelect />
);
}
```

View File

@ -1,72 +0,0 @@
# Overview
Most of the extensions for the NocoBase client are provided as Providers.
## Built-in Providers
- APIClientProvider
- I18nextProvider
- AntdConfigProvider
- SystemSettingsProvider
- PluginManagerProvider
- SchemaComponentProvider
- BlockSchemaComponentProvider
- AntdSchemaComponentProvider
- DocumentTitleProvider
- ACLProvider
## Registration of client-side Provider modules
Static Providers are registered with app.use() and dynamic Providers are adapted with dynamicImport.
```tsx | pure
import React from 'react';
import { Application } from '@nocobase/client';
const app = new Application({
apiClient: {
baseURL: process.env.API_BASE_URL,
},
dynamicImport: (name: string) => {
return import(`... /plugins/${name}`);
},
});
// When visiting the /hello page, display Hello world!
const HelloProvider = React.memo((props) => {
const location = useLocation();
if (location.pathname === '/hello') {
return <div>Hello world!</div>
}
return <>{props.children}</>
});
HelloProvider.displayName = 'HelloProvider'
app.use(HelloProvider);
```
## Client-side of plugins
Directory structure of the client-side of an empty plugin is as follows
```bash
|- /my-plugin
|- /src
|- /client
|- index.tsx
|- client.d.ts
|- client.js
```
``client/index.tsx`` reads as follows.
```tsx | pure
import React from 'react';
// This is an empty Provider, only children are passed, no custom Context is provided
export default React.memo((props) => {
return <>{props.children}</>;
});
```
After the plugin pm.add, it writes the `my-plugin.ts` file to the `packages/app/client/src/plugins` directory.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,75 +0,0 @@
# Plugin Settings Manager
<img src="./plugin-settings/settings-tab.jpg" style="max-width: 100%;"/>
## Example
### Basic Usage
```tsx | pure
import { Plugin } from '@nocobase/client';
import React from 'react';
const HelloSettingPage = () => <div>Hello Setting page</div>;
export class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'Hello', // menu title and page title
icon: 'ApiOutlined', // menu icon
Component: HelloSettingPage,
})
}
}
```
### Multiple Level Routes
```tsx | pure
import { Outlet } from 'react-router-dom'
const SettingPageLayout = () => <div> <div>This</div> public part, the following is the outlet of the sub-route: <div><Outlet /></div></div>;
class HelloPlugin extends Plugin {
async load() {
this.app.pluginSettingsManager.add('hello', {
title: 'HelloWorld',
icon: '',
Component: SettingPageLayout
})
this.app.pluginSettingsManager.add('hello.demo1', {
title: 'Demo1 Page',
Component: () => <div>Demo1 Page Content</div>
})
this.app.pluginSettingsManager.add('hello.demo2', {
title: 'Demo2 Page',
Component: () => <div>Demo2 Page Content</div>
})
}
}
```
### Get Route Path
If you want to get the jump link of the setting page, you can get it through the `getRoutePath` method.
```tsx | pure
import { useApp } from '@nocobase/client'
const app = useApp();
app.pluginSettingsManager.getRoutePath('hello'); // /admin/settings/hello
app.pluginSettingsManager.getRoutePath('hello.demo1'); // /admin/settings/hello/demo1
```
### Get Config
If you want to get the added configuration (already filtered by permissions), you can get it through the `get` method.
```tsx | pure
const app = useApp();
app.pluginSettingsManager.get('hello'); // { title: 'HelloWorld', icon: '', Component: HelloSettingPage, children: [{...}] }
```
See [samples/hello](https://github.com/nocobase/nocobase/blob/main/packages/plugins/%40nocobase/plugin-sample-hello/src/client/index.tsx) for full examples.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

View File

@ -1,40 +0,0 @@
# Testing
Testing is based on the [Jest](https://jestjs.io/) testing framework. Also included are common React testing libraries such as [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/)
## Example
```tsx | pure
import { render } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RouteSwitch } from '../RouteSwitch';
import { RouteSwitchProvider } from '../RouteSwitchProvider';
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
describe('route-switch', () => {
it('case 1', () => {
const App = () => {
return (
<RouteSwitchProvider components={{ Home, About }}>
<MemoryRouter initialEntries={['/']}>
<RouteSwitch
routes={[
{
type: 'route',
path: '/',
component: 'Home',
},
]}
/>
</MemoryRouter>
</RouteSwitchProvider>
);
};
const { container } = render(<App />);
expect(container).toMatchSnapshot();
});
});
```

View File

@ -1,66 +0,0 @@
# UI Routing
NocoBase Client's Router is based on [React Router](https://v5.reactrouter.com/web/guides/quick-start) and can be configured via `app.router` to configure ui routes with the following example.
```tsx
/**
* defaultShowCode: true
*/
import React from 'react';
import { Link, Outlet } from 'react-router-dom';
import { Application } from '@nocobase/client';
const Home = () => <h1>Home</h1>;
const About = () => <h1>About</h1>;
const Layout = () => {
return <div>
<div><Link to={'/'}>Home</Link>, <Link to={'/about'}>About</Link></div>
<Outlet />
</div>
}
const app = new Application({
router: {
type: 'memory',
initialEntries: ['/']
}
})
app.router.add('root', {
element: <Layout />
})
app.router.add('root.home', {
path: '/',
element: <Home />
})
app.router.add('root.about', {
path: '/about',
element: <About />
})
export default app.getRootComponent();
```
In a full NocoBase application, the Route can be extended in a similar way as follows.
```tsx | pure
import { Plugin } from '@nocobase/client';
class MyPlugin extends Plugin {
async load() {
// add
this.app.router.add('hello', {
path: '/hello',
element: <div>hello</div>,
})
// remove
this.app.router.remove('hello');
}
}
```
See [packages/samples/custom-page](https://github.com/nocobase/nocobase/tree/develop/packages/samples/custom-page) for the full example

View File

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

View File

@ -1,82 +0,0 @@
# Schema component library
## Wrapper Components
- BlockItem
- FormItem
- CardItem
## Layout
- Page
- Grid
- Tabs
- Space
## Field components
The field components are generally not used alone, but in the data presentation component
- CollectionField: universal component
- Cascader
- Checkbox
- ColorSelect
- DatePicker
- Filter
- Formula
- IconPicker
- Input
- InputNumber
- Markdown
- Password
- Percent
- Radio
- RecordPicker
- RichText
- Select
- TimePicker
- TreeSelect
- Upload
## Data presentation component
Need to be used with the field component
- Calendar
- Form
- Kanban
- Table
- TableV2
## Action (onClick event-based component)
- Action
- Action.Drawer
- Action.Modal
- ActionBarFor action layout
- Menu
## Other
- G2plot
- Markdown.Void
## Usage scenarios for `x-designer` and `x-initializer`
`x-designer` takes effect when `x-decorator` or `x-component` is a component of
- BlockItem
- CardItem
- FormItem
- Table.Column
- Tabs.TabPane
`x-initializer` takes effect when `x-decorator` or `x-component` is a component of
- ActionBar
- BlockItem
- CardItem
- FormItem
- Grid
- Table
- Tabs

View File

@ -1,110 +0,0 @@
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} />;
},
{ displayName: 'ArrayTable' },
);
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

@ -1,349 +0,0 @@
# Schema design capabilities
The design capabilities of Schema are mainly
- Neighborhood insertion, which can be used to
- Insertion of new schema nodes
- Drag-and-drop movement of existing schema nodes
- schema parameter modification
The core designer APIs and parameters are
- Designer API: `createDesignable()` & `useDesignable()`
- Schema parameters: `x-designer`, used to adapt the designer component
## Designer API
### createDesignable()
```ts
import { Schema } from '@nocobase/client';
const current = new Schema({
type: 'void',
'x-component': 'div',
});
const {
designable, // whether it is configurable
remove,
insertAdjacent, // insert at a position, four positions: beforeBegin, afterBegin, beforeEnd, afterEnd
insertBeforeBegin, // insert in front of the current node
insertAfterBegin, // insert in front of the first child node of the current node
insertBeforeEnd, // after the last child of the current node
insertAfterEnd, // after the current node
} = 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 scenarios can also use `useDesignable()` to get the API of the current schema component designer
```ts
const {
designable, // whether it is configurable
remove,
insertAdjacent, // insert at a position, four positions: beforeBegin, afterBegin, beforeEnd, afterEnd
insertBeforeBegin, // insert in front of the current node
insertAfterBegin, // insert in front of the first child node of the current node
insertBeforeEnd, // after the last child of the current node
insertAfterEnd, // after the current node
} = useDesignable();
const schema = {
name: uid(),
'x-component': 'Hello',
};
// Insert in front of the current node
insertBeforeBegin(schema);
// Equivalent to
insertAdjacent('beforeBegin', schema);
// insert in front of the first child of the current node
insertAfterBegin(schema);
// Equivalent to
insertAdjacent('afterBegin', schema);
// after the last child of the current node
insertBeforeEnd(schema);
// Equivalent to
insertAdjacent('beforeEnd', schema);
// After the current node
insertAfterEnd(schema);
// Equivalent to
insertAdjacent('afterEnd', schema);
```
## Neighborhood insertion
Similar to the DOM's [insert adjacent](https://dom.spec.whatwg.org/#insert-adjacent) concept, Schema also provides the `insertAdjacent()` method for solving the insertion of adjacent positions.
The four adjacent positions
```ts
{
properties: {
// beforeBegin insert before the current node
node1: {
properties: {
// afterBegin inserted before the first child of the current node
// ...
// beforeEnd after the last child of the current node
},
},
// afterEnd after the current node
},
}
```
Like HTML tags, the components of the Schema component library can be combined with each other and inserted in reasonable proximity as needed via the insertAdjacent API.
### Inserting a new schema node
Within a Schema component, a new node can be inserted directly into the adjacent position of the current Schema with `useDesignable()`.
Example
```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>
);
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
}, { displayName: 'Page' });
export default () => {
return (
<SchemaComponentProvider components={{ Page, Hello }}>
<SchemaComponent
schema={{
type: 'void',
name: 'page',
'x-component': 'Page',
properties: {
hello1: {
type: 'void',
'x-component': 'Hello',
},
},
}}
/>
</SchemaComponentProvider>
);
}
```
### Drag-and-drop movement of existing schema nodes
Methods such as insertAdjacent can also be used to drag and drop nodes
```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>;
}, { displayName: 'Page' });
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>
);
}, { displayName: 'Block' });
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>
);
}
```
## Applications of `x-designer`
`x-designer` is usually used only in wrapper components such as BlockItem, CardItem, FormItem, etc.
```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',
},
},
}
```
Note: The Schema designer provided by NocoBase is directly embedded in the interface in the form of a toolbar. When the UI configuration is activated (`designable = true`), the `x-designer` component (designer toolbar) will be displayed and the current schema component can be updated through the toolbar.
- Drag and Drop: DndContext + DragHandler
- Inserting new nodes: SchemaInitializer
- Parameter configuration: SchemaSettings

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -1,516 +0,0 @@
# Extending Schema Components
In addition to the native html tags, developers can also adapt more custom components to enrich the Schema component library.
Common methods used to extend components are
- [connect](https://react.formilyjs.org/api/shared/connect) to access third-party components without intrusion, generally used to adapt field components, and [mapProps](https://react.formilyjs.org/api/shared/map-props)[, mapReadPretty](https://react.formilyjs.org/api/shared/map-read-pretty) are used with
- [observer](https://react.formilyjs.org/api/shared/observer) when the component uses an observable object internally and you want the component to respond to changes to the observable object
## The simplest extension
Register a ready-made React component directly into it.
```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>
);
};
```
## Access to third-party components via 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>
);
};
```
## Using observer response data
```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>
}, { displayName: 'UsedObserver' });
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>
);
};
```
## Nested Schema
- `props.children` nesting for void and object types properties, see [nesting of void and object-type schema](#void-and-object-type-schema-nesting) for examples
- `<RecursionField />` custom nesting, for all types, see [nesting of array-type schema](#nesting-of-array-type-schema)
Note:
- `properties` of schema other than void and object types cannot be rendered directly by `props.children`, but nesting can be resolved using `<RecursionField />`
- Only schema of type void and object can be used with onlyRenderProperties
```tsx | pure
<RecursionField schema={schema} onlyRenderProperties />
```
### Nesting of void and object type schema
The properties node can be adapted directly via props.children
```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>
);
};
```
Comparison of rendering results by property type
```tsx
import React from 'react';
import { SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
const Hello = (props) => <h1>Hello, {props.children}!</h1>;
const World = () => <span>world</span>;
const schema = {
type: 'object',
properties: {
title1: {
type: 'void',
'x-content': 'Void schema渲染 properties',
},
void: {
type: 'void',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'void',
'x-component': 'World',
},
},
},
title2: {
type: 'void',
'x-content': 'Object schema渲染 properties',
},
object: {
type: 'object',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title3: {
type: 'void',
'x-content': 'Array schema不渲染 properties',
},
array: {
type: 'array',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
title4: {
type: 'void',
'x-content': 'String schema不渲染 properties',
},
string: {
type: 'string',
name: 'hello',
'x-component': 'Hello',
properties: {
world: {
type: 'string',
'x-component': 'World',
},
},
},
}
};
export default () => {
return (
<SchemaComponentProvider components={{ Hello, World }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
};
```
### Nesting of array type schema
Custom nesting can be solved with `<RecursionField />`
#### Array element is a string or 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) => {
// Only one element
return <RecursionField name={index} schema={schema} />
})}
</ul>
</>
);
}, { displayName: 'ArrayList' });
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>
);
};
```
#### When the Array element is an 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();
// The schema of array type cannot be onlyRenderProperties and needs to be converted to object type
const objSchema = new Schema({
type: 'object',
properties: schema.properties,
});
return (
<ul>
{field.value?.map((item, index) => {
// When the Array element is an Object
return (
<RecursionField name={index} schema={objSchema} onlyRenderProperties />
)
})}
</ul>
);
}, { displayName: 'ArrayList' });
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 structure data
```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} />;
}, { displayName: 'ArrayTable' });
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

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

View File

@ -1,144 +0,0 @@
# 邻近位置插入
与 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>
);
}, { displayName: 'Hello' });
const Page = observer((props) => {
return <div>{props.children}</div>;
}, { displayName: 'Page' });
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

@ -1,329 +0,0 @@
# What is UI Schema?
A protocol for describing front-end components, based on Formily Schema 2.0, JSON Schema-like style.
```ts
interface ISchema {
type: 'void' | 'string' | 'number' | 'object' | 'array';
name?: string;
title?: any;
// wrapper component
['x-decorator']? : string;
// Wrapper component properties
['x-decorator-props']? : any;
// component
['x-component']? : string;
// Component properties
['x-component-props']? : any;
// display state, default is 'visible'
['x-display']? : 'none' | 'hidden' | 'visible';
// child node of the component, simply use
['x-content']? : any;
// children node schema
properties?: Record<string, ISchema>;
// The following is used only for field components
// field linkage
['x-reactions']? : SchemaReactions;
// Field UI interaction mode, default is 'editable'
['x-pattern']? : 'editable' | 'disabled' | 'readPretty';
// Field validation
['x-validator']? : Validator;
// default data
default: ? :any;
// Designer related
// Designer component (toolbar), including: drag and drop to move, insert new nodes, modify parameters, remove, etc.
['x-designer']? : any;
// Initializer component (toolbar), determines what can be inserted inside the current schema
['x-initializer']? : any;
}
```
## The simplest component
All native html tags can be converted to schema writing. For example
```ts
{
type: 'void',
'x-component': 'h1',
'x-content': 'Hello, world!
}
```
JSX examples
```tsx | pure
<h1>Hello, world!</h1>
```
## children components can be written in properties
```ts
{
type: 'void',
'x-component': 'div',
'x-component-props': { className: 'form-item' },
properties: {
title: {
type: 'string',
'x-component': 'input',
},
},
}
```
JSX is equivalent to
```tsx | pure
<div className={'form-item'}>
<input name={'title'} />
</div>
```
## The clever use of Decorator
The combination of decorator + component allows you to put two components in a single schema node, reducing the complexity of the schema structure and increasing the reusability of the components.
For example, in a form scenario, you can combine a FormItem component with any field component, where the FormItem is the 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 is equivalent to
```tsx | pure
<div>
<FormItem>
<Input name={'title'} />
</FormItem>
<FormItem>
<Input.TextArea name={'content'} />
</FormItem>
</div>
```
It is also possible to provide a CardItem component that wraps all blocks, so that all blocks are Card wrapped.
```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 is equivalent to
```tsx | pure
<div>
<CardItem>
<Table />
</CardItem>
<CardItem>
<Kanban />
</CardItem>
</div>
```
## Display state of the component
- `'x-display': 'visible'`: the component is displayed
- `'x-display': 'hidden'`: component is hidden, data is not hidden
- `'x-display': 'none'`: component is hidden, data is also hidden
### `'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 is equivalent to
```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 is equivalent to
```tsx | pure
<div className={'form-item'}>
{/* The input component is not output here, the corresponding field model with name=title still exists */}
</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 is equivalent to
```tsx | pure
<div className={'form-item'}>
{/* The input component is not output here, and the corresponding field model with name=title does not exist anymore */}
</div>
```
## Display modes for components
For field components, there are three display modes:
- `'x-pattern': 'editable'` Editable
- `'x-pattern': 'disabled'` Non-editable
- `'x-pattern': 'readPretty'` Friendly reading
As in the case of the `<SingleText />` component, the editable and disabled modes are `<input />` and the readPretty mode is `<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 is equivalent to
```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 is equivalent to
```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 is equivalent to
```tsx | pure
<div className={'form-item'}>
<div>Hello</div>
</div>
```

View File

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

View File

@ -1,35 +0,0 @@
# x-initializer
## Built-in x-initializer component
- BlockInitializers
- CalendarActionInitializers
- CreateFormBlockInitializers
- CustomFormItemInitializers
- DetailsActionInitializers
- FormActionInitializers
- FormItemInitializers
- KanbanActionInitializers
- ReadPrettyFormActionInitializers
- ReadPrettyFormItemInitializers
- RecordBlockInitializers
- RecordFormBlockInitializers
- SubTableActionInitializers
- TableActionColumnInitializers
- TableActionInitializers
- TableColumnInitializers
- TableSelectorInitializers
- TabPaneInitializers
## Replacement
```tsx |pure
import React, { useContext } from 'react';
import { Plugin } from '@nocobase/client';
class MyPlugin extends Plugin {
async load() {
this.app.schemaInitializerManager.add('BlockInitializers', BlockInitializers)
}
}
```

View File

@ -1,105 +0,0 @@
# Dependency management
The dependencies of the plugin are divided into its own dependencies and global dependencies. Global dependencies are provided by `@nocobase/server` and `@nocobase/client`, and will not be packaged into the plugin product. Its own dependencies will be packaged into the product.
Because the dependencies of the plugin itself will be packaged into the product (including the npm packages that the server depends on, which will also be packaged into `dist/node_modules`), when developing the plugin, all dependencies should be placed in `devDependencies`.
<Alert type="warning">
When installing the following dependencies, pay attention to the **version** and keep consistent with `@nocobase/server` and `@nocobase/client`.
</Alert>
```js
// nocobase
'@nocobase/acl',
'@nocobase/actions',
'@nocobase/auth',
'@nocobase/cache',
'@nocobase/client',
'@nocobase/database',
'@nocobase/evaluators',
'@nocobase/logger',
'@nocobase/resourcer',
'@nocobase/sdk',
'@nocobase/server',
'@nocobase/test',
'@nocobase/utils',
// @nocobase/auth
'jsonwebtoken',
// @nocobase/cache
'cache-manager',
'cache-manager-fs-hash',
// @nocobase/database
'sequelize',
'umzug',
'async-mutex',
// @nocobase/evaluators
'@formulajs/formulajs',
'mathjs',
// @nocobase/logger
'winston',
'winston-daily-rotate-file',
// koa
'koa',
'@koa/cors',
'@koa/router',
'multer',
'@koa/multer',
'koa-bodyparser',
'koa-static',
'koa-send',
// react
'react',
'react-dom',
'react/jsx-runtime',
// react-router
'react-router',
'react-router-dom',
// antd
'antd',
'antd-style',
'@ant-design/icons',
'@ant-design/cssinjs',
// i18next
'i18next',
'react-i18next',
// dnd-kit
'@dnd-kit/accessibility',
'@dnd-kit/core',
'@dnd-kit/modifiers',
'@dnd-kit/sortable',
'@dnd-kit/utilities',
// formily
'@formily/antd-v5',
'@formily/core',
'@formily/react',
'@formily/json-schema',
'@formily/path',
'@formily/validator',
'@formily/shared',
'@formily/reactive',
'@formily/reactive-react',
// utils
'dayjs',
'mysql2',
'pg',
'pg-hstore',
'sqlite3',
'supertest',
'axios',
'@emotion/css',
'ahooks',
'lodash'
```

View File

@ -1,61 +0,0 @@
# Introduction
NocoBase adopts microkernel architecture, functions are extended in the form of plugins. Front and back ends are separated. Various plugin interfaces are provided, and plugins are divided by functional modules with pluggable features.
<img src="https://www.nocobase.com/images/NocoBaseMindMapLite.png" style="max-width: 800px;" >
The pluggable design reduces the coupling between modules and increases the reuse rate. As the plugin library continues to expand, common scenarios require only a combination of plugins to complete the base build. NocoBase's no-code platform, for example, is a combination of various plugins.
<img src="./index/pm-built-in.jpg" style="max-width: 800px;" />
## Plugin Manager
NocoBase provides a powerful plugin manager for managing plugins. The flow of the plugin manager is as follows
<img src="./index/pm-flow.svg" style="max-width: 580px;" />
No-code Users can manage the activation and deactivation of local plugins through the interface at
<img src="./index/pm-ui.jpg" style="max-width: 800px;" />
Developers can also manage the complete plugin process by way of the CLI:
```bash
# Create the plugin
yarn pm create hello
# Register the plugin
yarn pm add hello
# Activate the plugin
yarn pm enable hello
# Disable the plugin
yarn pm disable hello
# Remove the plugin
yarn pm remove hello
```
For more plugin examples, see [packages/samples](https://github.com/nocobase/nocobase/tree/main/packages/samples).
## Extensibility
Whether it is generic functionality or personalization, it is recommended to write it as a plugin. NocoBase is extensible in all aspects.
- It can be user-intuitive interface-related page modules, block types, action types, field types, etc.
- Filters, validators, access restrictions, etc. for enhancing or restricting the HTTP API
- It can also be enhancements to underlying data tables, migrations, events, command lines, etc.
Distribution of modules.
- Server
- Collections & Fields: mainly used for system table configuration. Business tables are recommended to be configured in "Plugin Settings Manager - Collection manager".
- Resources & Actions: Mainly used to extend the Action API
- Middleware: Middleware
- Events: Events
- I18n: server-side internationalization
- Commands: Custom command lines
- Migrations: Migration scripts
- Client
- UI Schema Designer: Page Designer
- UI Router: When there is a need for custom pages
- Plugin Settings Manager: Provides configuration pages for plugins
- I18n: Client side internationalization

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

View File

@ -1,125 +0,0 @@
# 学习路线指南
## 1. 从安装运行 NocoBase 开始
**相关文档:<a href="/welcome/getting-started/installation" target="_blank">快速开始</a>**
主要命令包括:
下载
```bash
yarn create/git clone
yarn install
```
安装
```bash
yarn nocobase install
```
运行
```bash
# for development
yarn dev
# for production
yarn build
yarn start
```
## 2. 了解 NocoBase 平台提供的核心功能
**相关文档:<a href="/manual" target="_blank">使用手册</a>**
主要的三部分包括:
- UI 设计器:主要包括区块、字段和操作
- 插件管理器:功能需求扩展
- 配置中心:已激活插件提供的配置功能
## 3. 进一步了解插件管理器的使用
**相关文档:<a href="/development" target="_blank">插件开发</a>**
NocoBase 提供了简易的插件管理器界面,但是在界面上只能处理本地插件的 enable、disable 和 remove完整的操作需要通过 CLI
```bash
# 创建插件
yarn pm create hello
# 注册插件
yarn pm add hello
# 激活插件
yarn pm enable hello
# 禁用插件
yarn pm disable hello
# 删除插件
yarn pm remove hello
```
更多插件示例,查看 packages/samples通过 samples 插件能够了解插件的基本用法,就可以进一步开发插件了。
## 4. 开发新插件,了解模块分布
**相关文档:<a href="/development/guide" target="_blank">扩展指南</a>**
[编写第一个插件](/development/your-fisrt-plugin) 章节,虽然简单的讲述了插件的主要开发流程,但是为了更快速的介入插件细节,你可能需要进一步了解 NocoBase 框架的模块分布:
- Server
- Collections & Fields主要用于系统表配置业务表建议在「配置中心 - 数据表配置」里配置
- Resources & Actions主要用于扩展 Action API
- Middleware中间件
- Events事件
- I18n服务端国际化
- Client
- UI Schema Designer页面设计器
- UI Router有自定义页面需求时
- Plugin Settings Manager为插件提供配置页面
- I18n客户端国际化
- Devtools
- Commands自定义命令行
- Migrations迁移脚本
## 5. 查阅各模块主要 API
**相关文档:<a href="/api" target="_blank">API 参考</a>**
查看各模块的 packages/samples进一步了解模块主要 API 的用法
- Server
- Collections & Fields
- db.collection
- db.import
- Resources & Actions
- app.resourcer.define
- app.resourcer.registerActions
- Middleware
- app.use
- app.acl.use
- app.resourcer.use
- Events
- app.on
- app.db.on
- I18n
- app.i18n
- ctx.i18n
- Client
- UI Schema Designer
- SchemaComponent
- SchemaInitializer
- SchemaSettings
- UI Router
- RouteSwitchProvider
- RouteSwitch
- I18n
- app.i18n
- useTranslation
- Devtools
- Commands
- app.command
- app.findCommand
- Migrations
- app.db.addMigration
- app.db.addMigrations

View File

@ -1,41 +0,0 @@
# Life cycle
## Lifecycle of applications
<img src="./index/app-flow.svg" style="max-width: 380px;" />
## Lifecycle of plugins
<img src="./index/pm-flow.svg" style="max-width: 600px;" />
## Lifecycle methods for plugins
```ts
import { InstallOptions, Plugin } from '@nocobase/server';
export class MyPlugin extends Plugin {
afterAdd() {
// After the plugin pm.add is registered. Mainly used to place the app.beforeLoad event.
beforeLoad() { }
beforeLoad() {
// Before all plugins are loaded. Generally used for registering classes and event listeners
}
async load() {
// Load configuration
}
async install(options?: InstallOptions) {
// Logic for installing
}
async afterEnable() {
// After activation
}
async afterDisable() {
// After disable
}
async remove() {
// Logic for removing
}
}
export default MyPlugin;
```

View File

@ -1 +0,0 @@
# Building

View File

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

View File

@ -1,45 +0,0 @@
# Plugin directory structure
An empty plugin can be created quickly with `yarn pm create my-plugin`, with the following directory structure.
```bash
|- /my-plugin
|- /src
|- /client # client-side of the plugin
|- /server # server-side of the plugin
|- client.d.ts
|- client.js
|- package.json # plugin package information
|- server.d.ts
|- server.js
|- build.config.ts # or `build.config.js`, modify configuration
```
The tutorial for `/src/server` refers to the [server](./server) section, and the tutorial for `/src/client` refers to the [client](./client) section.
If you want to customize the packaging configuration, you can create a `config.js` file in the root directory, with the following content:
```js
import { defineConfig } from '@nocobase/build';
export default defineConfig({
modifyViteConfig: (config) => {
// vite is used to package the `src/client` side code
// Modify the Vite configuration, for more information, please refer to: https://vitejs.dev/guide/
return config
},
modifyTsupConfig: (config) => {
// tsup is used to package the `src/server` side code
// Modify the tsup configuration, for more information, please refer to: https://tsup.egoist.dev/#using-custom-configuration
return config
},
beforeBuild: (log) => {
// The callback function before the build starts, you can do some operations before the build starts
},
afterBuild: (log: PkgLog) => {
// The callback function after the build is completed, you can do some operations after the build is completed
};
})
```

View File

@ -1,319 +0,0 @@
## Collections and Fields
## Basic Concepts
Data modeling is the lowest level foundation of an application. In NocoBase applications we model data through data tables (Collection) and fields (Field), and the modeling is also mapped to database tables for persistence.
### Collection
Collection is a collection of all similar data, which corresponds to the concept of database tables in NocoBase. Such as orders, products, users, comments, etc. can form a collection definition. Different collections are distinguished by name and contain fields defined by `fields`, such as
```ts
db.collection({
name: 'posts',
fields: [
{ name: 'title', type: 'string' }
{ name: 'content', type: 'text' },
// ...
]
});
```
The collection is only in memory after the definition, you need to call the [``db.sync()`'' (/api/database#sync) method to synchronize it to the database.
### Field
Corresponding to the concept of database table "fields", each data table (Collection) can have a number of Fields, for example.
```ts
db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'name' }
{ type: 'integer', name: 'age' }
// Other fields
],
});
```
The field name (`name`) and field type (`type`) are required, and different fields are distinguished by the field name (`name`). All field types and their configurations are described in the [List of built-in field types](/api/database/field#List of built-in field types) section of the API reference.
## Example
For developers, we usually build functional collections that are different from normal collections and solidify these collections as part of the plugin and combine them with other data processing processes to form a complete functionality.
Let's take a simple online store plugin as an example to show how to model and manage the collections of the plugin. Assuming you have already learned about [Develop your first plugin](/development/your-first-plugin), we continue to build on the previous plugin code, except that the name of the plugin is changed from `hello` to `shop-modeling`.
### Define and create collections in the plugin
For a store, you first need to create a collection of products, named `products`. Instead of calling [`db.collection()`](/api/database#collection) directly, in the plugin we will use a more convenient method to import multiple files of defined data tables at once. So let's start by creating a file for the product collection definition named ``collections/products.ts`` and fill it with the following content.
```ts
export default {
name: 'products',
fields: [
{
type: 'string',
name: 'title'
},
{
type: 'integer',
name: 'price'
},
{
type: 'boolean',
name: 'enabled'
},
{
type: 'integer',
name: 'inventory'
}
]
};
```
As you can see, the collections structure definition can be used directly in standard JSON format, where `name` and `fields` are required representing the collection's name and the field definitions in the collection. Field definitions similar to Sequelize create system fields such as primary key (`id`), data creation time (`createdAt`) and data update time (`updatedAt`) by default, which can be overridden by a configuration with the same name if there is a special need.
The data table defined in this file we can introduce and complete the definition in the `load()` cycle of the main plugin class using `db.import()`. This is shown below.
```ts
import path from 'path';
import { Plugin } from '@nocobase/server';
export default class ShopPlugin extends Plugin {
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.acl.allow('products', '*');
this.app.acl.allow('categories', '*');
this.app.acl.allow('orders', '*');
}
}
```
In the meantime, for testing purposes, we will temporarily allow all access permissions for the data in these collections, and later we will detail how to manage data permissions in [Permissions Management](/development/guide/acl).
This way, when the plugin is loaded by the main application, the `products` collection we defined is also loaded into the memory of the database management instance. At the same time, the NocoBase constraint-based resource mapping of the collections automatically generates the corresponding CRUD HTTP API after the application's service is started.
When the following URLs are requested from the client, the corresponding responses are obtained.
* `GET /api/products:list`: Get a list of all product data
* `GET /api/products:get?filterByTk=<id>`: Get the product data for the specified ID
* `POST /api/products`: Create a new product data
* `PUT /api/products:update?filterByTk=<id>`: Update a product data
* `DELETE /api/products:destroy?filterByTk=<id>`: Delete a product data
### Defining associated collections and fields
In the above example, we only defined a product collection, but in reality a product also needs to be associated to a category, a brand, a supplier, etc. For example, we can define a `categories` collection to store the categories, and then add a `category` field to the product collection to associate it with the category collection.
Add a new file `collections/categories.ts` and fill in the content.
```ts
export default {
name: 'categories',
fields: [
{
type: 'string',
name: 'title'
},
{
type: 'hasMany',
name: 'products',
}
]
};
```
We have defined two fields for the `categories` collection, one for the title and another one-to-many field for all the products associated under that category, which will be described later. Since we have already used the `db.import()` method in the plugin's main class to import all the data table definitions under the `collections` directory, the new `categories` collection added here will also be automatically imported into the database management instance.
Modify the file `collections/products.ts`` to add a `category` field to the `fields`.
```ts
{
name: 'products',
fields: [
// ...
{
type: 'belongsTo',
name: 'category',
target: 'categories',
}
]
}
```
As you can see, the `category` field we added to the `products` collection is a `belongsTo` type field, and its `target` property points to the `categories` collection, thus defining a many-to-one relationship between the `products` collection and the `categories` collection. Combined with the `hasMany` field defined in the `categories` collection, we can achieve a relationship where one product can be associated to multiple categories and multiple products under one category. Usually `belongsTo` and `hasMany` can appear in pairs, defined in two separate collections.
Once the relationship between the two collections is defined, we can also request the associated data directly through the HTTP API
* `GET /api/products:list?appends=category`: Get all products data, including the associated categories data
* `GET /api/products:get?filterByTk=<id>&appends=category`: Get the product data for the specified ID, including the associated category data.
* `GET /api/categories/<categoryId>/products:list`: Get all the products under the specified category
* `POST /api/categories/<categoryId>/products`: Create a new product under the specified category
Similar to the general ORM framework, NocoBase has four built-in relational field types, for more information you can refer to the section about API field types.
* [`belongsTo` type](/api/database/field#belongsto)
* [`belongsToMany` type](/api/database/field#belongstomany)
* [`hasMany` type](/api/database/field#hasmany)
* [`hasOne` type](/api/database/field#hasone)
### Extend an existing collection
In the above example, we already have a product collection and a category collection, in order to provide the sales process we also need an order collection. We can add a new `orders.ts` file to the `collections` directory and define an `orders` collection as follows
```ts
export default {
name: 'orders',
fields: [
{
type: 'uuid',
name: 'id',
primaryKey: true
},
{
type: 'belongsTo',
name: 'product'
},
{
type: 'integer',
name: 'quantity'
},
{
type: 'integer',
name: 'totalPrice'
},
{
type: 'integer',
name: 'status'
},
{
type: 'string',
name: 'address'
},
{
type: 'belongsTo',
name: 'user'
}
]
}
```
For the sake of simplicity, the association between the order collection and the product collection we simply define as a many-to-one relationship, while in the actual business may be used in a complex modeling approach such as many-to-many or snapshot. As you can see, an order in addition to corresponding to a commodity, we also added a relationship definition corresponding to the users, which is a collection managed by the NocoBase built-in plugins (refer to [code for users plugin](https://github.com/nocobase/nocobase/tree/main/packages/) for details plugins/users)). If we want to extend the definition of the "multiple orders owned by a user" relationship for the existing users collection, we can continue to add a new collection file `collections/users.ts` inside the current shop-modeling plugin, which is different from exporting the JSON collection directly. Unlike the direct export of a JSON, the `@nocobase/database` package's `extend()` method is used here to extend the definition of an existing collection: ``ts
```ts
import { extend } from '@nocobase/database';
export extend({
name: 'users',
fields: [
{
type: 'hasMany',
name: 'orders'
}
]
});
```
This way, the existing users table also has an `orders` associated field, and we can retrieve all the order data for a given user via `GET /api/users/<userId>/orders:list`.
This method is very useful when extending collections already defined by other plugins, so that other existing plugins do not depend on the new plugin in reverse, but only form one-way dependencies, facilitating a certain degree of decoupling at the extension level.
### Extend field types
We use `uuid` type for `id` field when we define order table, which is a built-in field type. Sometimes we may feel that UUID looks too long and waste space, and the query performance is not good, we want to use a more suitable field type, such as a complex numbering logic with date information, or Snowflake algorithm, we need to extend a custom field type.
Suppose we need to apply the Snowflake ID generation algorithm directly to extend a ``snowflake`` field type, we can create a ``fields/snowflake.ts`` file.
```ts
// Import the algorithm toolkit
import { Snowflake } from 'nodejs-snowflake';
// Import field type base class
import { DataTypes, Field, BaseColumnFieldOptions } from '@nocobase/database';
export interface SnowflakeFieldOptions extends BaseColumnFieldOptions {
type: 'snowflake';
epoch: number;
instanceId: number;
}
export class SnowflakeField extends Field {
get dataType() {
return DataTypes.BIGINT;
}
constructor(options: SnowflakeFieldOptions, context) {
super(options, context);
const {
epoch: custom_epoch,
instanceId: instance_id = process.env.INSTANCE_ID ? Number.parseInt(process.env.INSTANCE_ID) : 0,
} = options;
this.generator = new Snowflake({ custom_epoch, instance_id });
}
setValue = (instance) => {
const { name } = this.options;
instance.set(name, this.generator.getUniqueID());
};
bind() {
super.bind();
this.on('beforeCreate', this.setValue);
}
unbind() {
super.unbind();
this.off('beforeCreate', this.setValue);
}
}
export default SnowflakeField;
```
Afterwards, register the new field type into the collection in the main plugin file.
```ts
import SnowflakeField from '. /fields/snowflake';
export default class ShopPlugin extends Plugin {
initialize() {
// ...
this.db.registerFieldTypes({
snowflake: SnowflakeField
});
// ...
}
}
```
This allows us to use the `snowflake` field type in the order table:
```ts
export default {
name: 'orders',
fields: [
{
type: 'snowflake'
name: 'id',
primaryKey: true
},
// ... . other fields
]
}
```
## Summary
With the above example, we basically understand how to model data in a plugin, including.
* Defining collections and common fields
* Defining association collections and fields relationships
* Extending fields of an existing collections
* Extending new field types
We have put the code covered in this chapter into a complete sample package [packages/samples/shop-modeling](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-modeling), which can be run directly locally to see the results.

View File

@ -1,153 +0,0 @@
# Association Fields
In a relational database, the standard way to build a table relationship is to add a foreign key field followed by a foreign key constraint. For example, Knex builds a table with the following example.
```ts
knex.schema.table('posts', function (table) {
table.integer('userId').unsigned();
table.foreign('userId').references('users.id');
});
```
This procedure creates a userId field in the posts table and sets the foreign key constraint posts.userId to refer to users.id. In NocoBase's Collection, such a relational constraint is created by configuring the relational field, e.g.
```ts
{
name: 'posts',
fields: [
{
type: 'belongsTo',
name: 'user',
target: 'users',
foreignKey: 'userId',
},
],
}
```
## Relationship parameters
### BelongsTo
```ts
interface BelongsTo {
type: 'belongsTo';
name: string;
// defaults to name's plural
target?: string;
// The default value is the primary key of the target model, usually 'id'
targetKey?: any;
// defaults to target + 'Id'
foreignKey?: any;
}
// The authors table's primary key id is concatenated with the books table's foreign key authorId
{
name: 'books',
fields: [
{
type: 'belongsTo',
name: 'author',
target: 'authors',
targetKey: 'id', // authors table's primary key
foreignKey: 'authorId', // foreign key in books table
}
],
}
```
### HasOne
```ts
interface HasOne {
type: 'hasOne';
name: string;
// defaults to name's plural
target?: string;
// The default value is the primary key of the source model, usually 'id'
sourceKey?: string;
// default value is the singular form of source collection name + 'Id'
foreignKey?: string;
foreignKey?}
// The users table's primary key id is concatenated with the profiles' foreign key userId
{
name: 'users',
fields: [
{
type: 'hasOne',
name: 'profile',
target: 'profiles',
sourceKey: 'id', // users table's primary key
foreignKey: 'userId', // foreign key in profiles table
}
],
}
```
### HasMany
```ts
interface HasMany {
type: 'hasMany';
name: string;
// defaults to name
target?: string;
// The default value is the primary key of the source model, usually 'id'
sourceKey?: string;
// the default value is the singular form of the source collection name + 'Id'
foreignKey?: string;
}
// The posts table's primary key id is concatenated with the comments table's postId
{
name: 'posts',
fields: [
{
type: 'hasMany',
name: 'comments',
target: 'comments',
sourceKey: 'id', // posts table's primary key
foreignKey: 'postId', // foreign key in the comments table
}
],
}
```
### BelongsToMany
```ts
interface BelongsToMany {
type: 'belongsToMany';
name: string;
// default value is name
target?: string;
// defaults to the source collection name and target in the natural order of the first letter of the string
through?: string;
// defaults to the singular form of source collection name + 'Id'
foreignKey?: string;
// The default value is the primary key of the source model, usually id
sourceKey?: string;
// the default value is the singular form of target + 'Id'
otherKey?: string;
// the default value is the primary key of the target model, usually id
targetKey?: string;
}
// tags table's primary key, posts table's primary key and posts_tags two foreign keys are linked
{
name: 'posts',
fields: [
{
type: 'believesToMany',
name: 'tags',
target: 'tags',
through: 'posts_tags', // intermediate table
foreignKey: 'tagId', // foreign key 1, in posts_tags table
otherKey: 'postId', // foreignKey2, in posts_tags table
targetKey: 'id', // tags table's primary key
sourceKey: 'id', // posts table's primary key
}
],
}
```

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.2 KiB

View File

@ -1,82 +0,0 @@
# Collection templates
<Alert>
📢 Collection templates are scheduled to be available in Q4 2022.
</Alert>
In real business scenarios, different collections may have their own initialization rules and business logic, and NocoBase addresses such issues by providing collection templates.
## General collections
```ts
db.collection({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
}
],
});
```
## Tree structure collections
```ts
db.collection({
name: 'categories',
tree: 'adjacency-list',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'description',
},
{
type: 'belongsTo',
name: 'parent',
target: 'categories',
foreignKey: 'parentId',
},
{
type: 'hasMany',
name: 'children',
target: 'categories',
foreignKey: 'parentId',
},
],
});
```
## Parent-child inheritance collections
```ts
db.collection({
name: 'a',
fields: [
],
});
db.collection({
name: 'b',
inherits: 'a',
fields: [
],
});
```
## More templates
As in the case of calendar collections, each initialized collection needs to be initialized with special cron and exclude fields, and the definition of such fields is done by the template
```ts
db.collection({
name: 'events',
template: 'calendar',
});
```

View File

@ -1,62 +0,0 @@
# How to configure collections?
NocoBase has three ways to configure collections.
<img src="./cm.svg" style="max-width: 800px;" />
## Configuring collections through the interface
Business data is generally recommended to be configured using the interface, and the NocoBase platform provides two interfaces to configure collections.
### Regular table interface
<img src="./table.jpg" style="max-width: 800px;" />
### Graphical configuration interface
<img src="./graph.jpg" style="max-width: 800px;" />
## Defined in the plugin code
Generally used to configure plugin functions or system configuration tables where users can read and write data, but cannot modify the data structure.
```ts
export class MyPlugin extends Plugin {
load() {
this.db.collection();
this.db.import();
}
}
```
Related API Reference
- [db.collection()](/api/database#collection)
- [db.import()](/api/database#import)
The collection configured in the plugin is automatically synchronized with the database when the plugin is activated, giving birth to the corresponding data tables and fields.
## Managing data tables via REST API
Third parties can also manage data tables via the HTTP interface (permissions required)
### Collections
```bash
GET /api/collections
POST /api/collections
GET /api/collections/<collectionName>
PUT /api/collections/<collectionName>
DELETE /api/collections/<collectionName>
```
### Collection fields
```bash
GET /api/collections/<collectionName>/fields
POST /api/collections/<collectionName>/fields
GET /api/collections/<collectionName>/fields/<fieldName>
PUT /api/collections/<collectionName>/fields/<fieldName>
DELETE /api/collections/<collectionName>/fields/<fieldName>
```

View File

@ -1,39 +0,0 @@
# How to extend fields
The composition of a Collection Field in NocoBase consists of
<img src="./collection-field.svg" />
## Extend Field Type
For example, to extend the password type field ``type: 'password'`
```ts
export class MyPlugin extends Plugin {
beforeLoad() {
this.db.registerFieldTypes({
password: PasswordField
});
}
}
export class PasswordField extends Field {
get dataType() {
return DataTypes.STRING;
}
}
```
- [More implementations of the built-in field types can be found here](https://github.com/nocobase/nocobase/tree/main/packages/core/database/src/fields)
- Also see the full samples plugin [packages/samples/shop-modeling](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-modeling)
## Extend Field Component
Related extension documentation can be found at
- [Extending Schema Components](/development/client/ui-schema-designer/extending-schema-components)
- [Schema component library](/development/client/ui-schema-designer/component-library)
## Extend Field Interface
- [Built-in field interfaces view here](https://github.com/nocobase/nocobase/tree/main/packages/core/client/src/collection-manager/interfaces)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

View File

@ -1,194 +0,0 @@
# Core concepts
## Collection
Collection is a collection of all kinds of data, such as orders, products, users, comments, etc. Different collections are distinguished by name, e.g.
```ts
// Orders
{
name: 'orders',
}
// Products
{
name: 'products',
}
// Users
{
name: 'users',
}
// Comments
{
name: 'comments',
}
```
## Collection Field
Each Collection has a number of Fields.
```ts
// Collection configuration
{
name: 'users',
fields: [
{ type: 'string', name: 'name' },
{ type: 'integer', name: 'age' },
// Other fields
],
}
// sample data
[
{
name: 'Jason',
age: 20,
},
{ {
name: 'Li Si',
age: 18,
}
];
```
The composition of a Collection Field in NocoBase consists of
<img src="./collection-field.svg" />
### Field Type
Different fields are distinguished by name, and type indicates the data type of the field, which is divided into Attribute Type and Association Type, e.g.
**Attribute - Attribute Type**
- string
- text
- date
- boolean
- time
- float
- json
- location
- password
- virtual
- ...
**Relationship - Association Type**
- hasOne
- hasMany
- belongsTo
- belongsToMany
- ...
### Field Component
The field has a data type, the IO of the field value is fine, but it is not enough, if you need to display the field on the interface, you need another dimension of configuration -- `uiSchema`, e.g.
```tsx | pure
// Email field, displayed with Input component, using email validation rules
{
type: 'string',
name: 'email',
uiSchema: {
'x-component': 'Input',
'x-component-props': { size: 'large' },
'x-validator': 'email',
'x-pattern': 'editable', // editable state, and readonly state, read-pretty state
},
}
// Example data
{
email: 'admin@nocobase.com',
}
// Component example
<Input name={'email'} size={'large'} value={'admin@nocobase.com'} />
```
The uiSchema is used to configure the components of the field to be displayed on the interface, each field component will correspond to a value and includes several maintained configurations:
- The component of the field
- The parameters of the component
- The field's validation rules
- The mode of the field (editable, readonly, read-pretty)
- The default value of the field
- Other
[see the UI Schema chapter for more information](/development/client/ui-schema-designer/what-is-ui-schema).
The built-in field components of NocoBase are
- Input
- InputNumber
- Select
- Radio
- Checkbox
- ...
### Field Interface
With Field Type and Field Component you can freely combine several fields, we call this combined template Field Interface, e.g.
```ts
// email field, string + input, email validation rules
{
type: 'string',
name: 'email',
uiSchema: {
'x-component': 'Input',
'x-component-props': {},
'x-validator': 'email',
},
}
// phone field, string + input, phone validation rules
{
type: 'string',
name: 'phone',
uiSchema: {
'x-component': 'Input',
'x-component-props': {},
'x-validator': 'phone',
},
}
```
The above email and phone require a full uiSchema to be configured each time which is very tedious. To simplify the configuration, another concept Field interface is introduced, which can template some parameters, e.g.
```ts
// Template for the email field
interface email {
type: 'string';
uiSchema: {
'x-component': 'Input',
'x-component-props': {},
'x-validator': 'email',
};
}
// Template for the phone field
interface phone {
type: 'string';
uiSchema: {
'x-component': 'Input',
'x-component-props': {},
'x-validator': 'phone',
};
}
// Simplified field configuration
// email
{
interface: 'email',
name: 'email',
}
// phone
{
interface: 'phone',
name: 'phone',
}
```
[More Field Interface here](https://github.com/nocobase/nocobase/tree/main/packages/core/client/src/collection-manager/interfaces)

View File

@ -1,137 +0,0 @@
# Collection protocol
Collection is the backbone of NocoBase, a protocol for describing data structures (collections and fields), very close to the concept of a relational database, but not limited to relational databases, but can also be a data source for NoSQL databases, HTTP APIs, etc.
<img src="./schema.svg" style="max-width: 800px;" >
At this stage, the Collection protocol is based on the relational database interface (db.collections), and data sources such as NoSQL databases and HTTP APIs will be implemented gradually in the future.
Collection protocol mainly includes two parts: CollectionOptions and FieldOptions. Because Field is extensible, the parameters of FieldOptions are very flexible.
## CollectionOptions
```ts
interface CollectionOptions {
name: string;
title?: string;
// Tree structure table, TreeRepository
tree?: 'adjacency-list' | 'closure-table' | 'materialized-path' | 'nested-set';
// parent-child inheritance
inherits?: string | string[];
fields?: FieldOptions[];
timestamps?: boolean;
paranoid?: boolean;
sortable?: CollectionSortable;
model?: string;
repository?: string;
[key: string]: any;
}
type CollectionSortable = string | boolean | { name?: string; scopeKey?: string };
```
## FieldOptions
Generic field parameters
```ts
interface FieldOptions {
name: string;
type: string;
hidden?: boolean;
index?: boolean;
interface?: string;
uiSchema?: ISchema;
```
[Introduction to UI Schema here](/development/client/ui-schema-designer/what-is-ui-schema)
### Field Type
Field Type includes Attribute Type and Association Type.
**Attribute Type**
- 'boolean'
- 'integer'
- 'bigInt'
- 'double'
- 'real'
- 'decimal'
- 'string'
- 'text'
- 'password'
- 'date'
- 'time'
- 'array'
- 'json'
- 'jsonb'
- 'uuid'
- 'uid'
- 'formula'
- 'radio'
- 'sort'
- 'virtual'
**Association Type**
- 'belongsTo'
- 'hasOne'
- 'hasMany'
- 'belongsToMany'
### Field Interface
**Basic**
- input
- textarea
- phone
- email
- integer
- number
- percent
- password
- icon
**Choices**
- checkbox
- select
- multipleSelect
- radioGroup
- checkboxGroup
- chinaRegion
**Media**
- attachment
- markdown
- richText
**Date & Time**
- datetime
- time
**Relation**
- linkTo - `type: 'believesToMany'`
- oho - `type: 'hasOne'`
- obo - `type: 'believesTo'`
- o2m - `type: 'hasMany'`
- m2o - `type: 'believesTo'`
- m2m - `type: 'believesToMany'`
**Advanced**
- formula
- sequence
**System info**
- id
- createdAt
- createdBy
- updatedAt
- updatedBy

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

View File

@ -1,100 +0,0 @@
# Commands
NocoBase Server Application is a powerful and extensible CLI tool in addition to being used as a WEB server.
Create a new `app.js` file with the following code.
```ts
const Application = require('@nocobase/server');
// omit the specific configuration here
const app = new Application({/*... */});
app.runAsCLI();
```
app.js run as ``runAsCLI()`` is a CLI, it will work like this in the command line tool.
```bash
node app.js install # install
node app.js start # start
```
To better develop, build and deploy NocoBase applications, NocoBase has many built-in commands, see the [NocoBase CLI](/api/cli) section for details.
## How to customize Command?
NocoBase CLI is designed to be very similar to [Laravel Artisan](https://laravel.com/docs/9.x/artisan), both are extensible. NocoBase CLI is based on [commander](https://www.npmjs.com/ package/commander) implementation, which extends Command like this
```ts
export class MyPlugin extends Plugin {
load() {
this.app
.command('echo')
.option('--v, --version');
.action(async ([options]) => {
console.log('Hello World!');
if (options.version) {
console.log('Current version:', app.getVersion());
}
});
}
}
```
This method defines the following command.
```bash
yarn nocobase echo
# Hello World!
yarn nocobase echo -v
# Hello World!
# Current version: 0.8.0-alpha.1
```
More API details can be found in the [Application.command()](/api/server/application#command) section.
## Example
### Defining a command for exporting collections
If we want to export the data in the application's collections to a JSON file, we can define a subcommand as follows.
```ts
import path from 'path';
import * as fs from 'fs/promises';
class MyPlugin extends Plugin {
load() {
this.app
.command('export')
.option('-o, --output-dir')
.action(async (options, . .collections) => {
const { outputDir = path.join(process.env.PWD, 'storage') } = options;
await collections.reduce((promise, collection) => promise.then(async () => {
if (!this.db.hasCollection(collection)) {
console.warn('No such collection:', collection);
return;
}
const repo = this.db.getRepository(collection);
const data = repo.find();
await fs.writeFile(path.join(outputDir, `${collection}.json`), JSON.stringify(data), { mode: 0o644 });
}), Promise.resolve());
});
}
}
```
After registering and activating the plugin call from the command line.
```bash
mkdir -p . /storage/backups
yarn nocobase export -o . /storage/backups users
```
After execution, it will generate `. /storage/backups/users.json` file containing the data from the collections.
## Summary
The sample code covered in this chapter is integrated in the [packages/samples/command](https://github.com/nocobase/nocobase/tree/main/packages/samples/command) package and can be run directly locally to see the results.

View File

@ -1,168 +0,0 @@
# Events
NocoBase provides a very large number of event listeners in the lifecycle of applications, plugins, and database, and these methods will only be executed when an event is triggered.
## How to add event listeners?
The registration of events is usually placed in afterAdd or beforeLoad
```ts
export class MyPlugin extends Plugin {
// After the plugin is added, afterAdd() is executed with or without activation
afterAdd() {
this.app.on();
this.db.on();
}
// beforeLoad() will only be executed after the plugin is activated
beforeLoad() {
this.app.on();
this.db.on();
}
}
```
### `db.on`
Database related events are related to Collection configuration, CRUD of Repository, including:
- 'beforeSync' / 'afterSync'
- 'beforeValidate' / 'afterValidate'
- 'beforeCreate' / 'afterCreate'
- 'beforeUpdate' / 'afterUpdate'
- 'beforeSave' / 'afterSave'
- 'beforeDestroy' / 'afterDestroy'
- 'afterCreateWithAssociations'
- 'afterUpdateWithAssociations'
- 'afterSaveWithAssociations'
- 'beforeDefineCollection'
- 'afterDefineCollection'
- 'beforeRemoveCollection' / 'afterRemoveCollection'
See [Database API](/api/database) for more details.
### `app.on()`
The app's events are related to the application's lifecycle, and the relevant events are:
- 'beforeLoad' / 'afterLoad'
- 'beforeInstall' / 'afterInstall'
- 'beforeUpgrade' / 'afterUpgrade'
- 'beforeStart' / 'afterStart'
- 'beforeStop' / 'afterStop'
- 'beforeDestroy' / 'afterDestroy'
Refer to [Application API](/api/server/application#Events) for more details.
## Example
Let's continue with a simple online store as an example, the related collections modeling can be reviewed in the [Collections and Fields](/development/) section for examples.
### Deducting product inventory after creating an order
Usually we have different collections for products and orders. The problem of overselling can be solved by subtracting the inventory of the item after the customer has placed the order. At this point we can define the corresponding event for the action of Creating Order and solve the inventory modification problem at this time together with:
```ts
class ShopPlugin extends Plugin {
beforeLoad() {
this.db.on('orders.afterCreate', async (order, options) => {
const product = await order.getProduct({
transaction: options.transaction
});
await product.update({
inventory: product.inventory - order.quantity
}, {
transaction: options.transaction
});
});
}
}
```
Since the default Sequelize event carries information about the transaction, we can use transaction directly to ensure that both data actions are performed in the same transaction.
Similarly, you can change the order status to shipped after creating the shipping record: ```ts
```ts
class ShopPlugin extends Plugin {
load() {
this.db.on('deliveries.afterCreate', async (delivery, options) => {
const orderRepo = this.db.getRepository('orders');
await orderRepo.update({
filterByTk: delivery.orderId,
value: {
status: 2
}
transaction: options.transaction
});
});
}
}
```
### Timed tasks that exist alongside the application
Without considering complex cases such as using workflow plugins, we can also implement a simple timed task mechanism via application-level events, and it can be bound to the application's process and stop when it exits. For example, if we want to scan all orders at regular intervals and automatically sign them after the sign-off time.
```ts
class ShopPlugin extends Plugin {
timer = null;
orderReceiveExpires = 86400 * 7;
checkOrder = async () => {
const expiredDate = new Date(Date.now() - this.orderReceiveExpires);
const deliveryRepo = this.db.getRepository('deliveries');
const expiredDeliveries = await deliveryRepo.find({
fields: ['id', 'orderId'],
filter: {
status: 0,
createdAt: {
$lt: expiredDate
}
}
});
await deliveryRepo.update({
filter: {
id: expiredDeliveries.map(item => item.get('id')),
},
values: {
status: 1
}
});
const orderRepo = this.db.getRepository('orders');
const [updated] = await orderRepo.update({
filter: {
status: 2,
id: expiredDeliveries.map(item => item.get('orderId'))
},
values: {
status: 3
}
});
console.log('%d orders expired', updated);
};
load() {
this.app.on('beforeStart', () => {
// execute every minute
this.timer = setInterval(this.checkOrder, 1000 * 60);
});
this.app.on('beforeStop', () => {
clearInterval(this.timer);
this.timer = null;
});
}
}
```
## Summary
The above example gives us a basic understanding of what events do and the ways they can be used to extend.
* Database related events
* Application related events
The sample code covered in this chapter is integrated in the corresponding package [packages/samples/shop-events](https://github.com/nocobase/nocobase/tree/main/packages/samples/shop-events), which can be run directly in run locally to see the results.

View File

@ -1,134 +0,0 @@
# Internationalization
Internationalization in NocoBase is implemented based on [i18next](https://npmjs.com/package/i18next).
## How to register a multilingual package?
```ts
export class MyPlugin extends Plugin {
load() {
this.app.i18n.addResources('zh-CN', 'test', {
Hello: '你好',
World: '世界',
});
this.app.i18n.addResources('en-US', 'test', {
Hello: 'Hello',
World: 'World',
});
}
}
```
## Two i18n instances
### app.i18n
Global i18n instance, typically used in the CLI.
```ts
app.i18n.t('World') // "世界" or "World"
```
### ctx.i18n
CloneInstance of global i18n with a completely independent context for each request, typically used to respond to multilingual messages based on the client language.
```ts
app.use(async (ctx, next) => {
ctx.body = `${ctx.i18n.t('Hello')} ${ctx.i18n.t('World')}`;
await next();
});
```
The client request parameters can be placed in the query string
```bash
GET /?locale=en-US HTTP/1.1
Host: localhost:13000
```
or in the request headers
```bash
GET / HTTP/1.1
Host: localhost:13000
X-Locale: en-US
```
## Suggested configuration
With English text as the key and translation as the value, this has the advantage that even if multiple languages are missing, it will be displayed in English and will not cause reading barriers, e.g.
```ts
i18n.addResources('zh-CN', 'your-namespace', {
'Show dialog': '显示对话框',
'Hide dialog': '隐藏对话框'
});
```
To make it easier to manage multilingual files, it is recommended to create a `locale` folder in the plugin and place all the corresponding language files in it:
```bash
|- /my-plugin
|- /src
|- /server
|- locale # Multi-language folder
|- en-cn.ts
|- en-US.ts
```
## Example
### Server-side error alert
For example, when a user places an order for a product in the store, if the product is not in stock, or not on the shelf, then the order interface should return the appropriate error when it is called.
```ts
const namespace = 'shop';
export default class ShopPlugin extends Plugin {
async load() {
this.app.i18n.addResources('zh-CN', namespace, {
'No such product': '商品不存在',
'Product not on sale': '商品已下架',
'Out of stock': '库存不足',
});
this.app.resource({
name: 'orders',
actions: {
async create(ctx, next) {
const productRepo = ctx.db.getRepository('products');
const product = await productRepo.findOne({
filterByTk: ctx.action.params.values.productId
productId });
if (!product) {
return ctx.throw(404, ctx.t('No such product'));
}
if (!product.enabled) {
return ctx.throw(400, ctx.t('Product not on sale'));
}
if (!product.inventory) {
return ctx.throw(400, ctx.t('Out of stock'));
}
const orderRepo = ctx.db.getRepository('orders');
ctx.body = await orderRepo.create({
values: {
productId: product.id,
quantity: 1,
totalPrice: product.price,
userId: ctx.state.currentUser.id
}
});
next();
}
}
});
}
}
```

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