fix: delete root docs (#3145)
* fix: delete root docs * docs: update useSchemaOptionsContext() * chore: change ci * chore: upgrade dumi theme * fix: upgrade tsx
@ -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;
|
@ -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;
|
@ -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;
|
61
.dumirc.ts
@ -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, // 报错
|
||||
});
|
2
.github/workflows/deploy-client-docs.yml
vendored
@ -5,12 +5,14 @@ on:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'packages/core/client/**'
|
||||
- 'packages/core/client/docs/**'
|
||||
- '.github/workflows/deploy-client-docs.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'packages/core/client/**'
|
||||
- 'packages/core/client/docs/**'
|
||||
- '.github/workflows/deploy-client-docs.yml'
|
||||
|
||||
jobs:
|
||||
|
296
docs/config.ts
@ -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 };
|
@ -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.
|
@ -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
|
@ -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
|
@ -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 |
|
@ -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 mode:Used 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
|
||||
```
|
@ -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();
|
||||
```
|
@ -1,23 +0,0 @@
|
||||
# ACL
|
||||
|
||||
## Components
|
||||
|
||||
### `<ACLProvider />`
|
||||
|
||||
### `<ACLRolesCheckProvider />`
|
||||
|
||||
### `<ACLCollectionProvider />`
|
||||
|
||||
### `<ACLActionProvider />`
|
||||
|
||||
### `<ACLCollectionFieldProvider />`
|
||||
|
||||
### `<ACLMenuItemProvider />`
|
||||
|
||||
## Hooks
|
||||
|
||||
### `useACLContext()`
|
||||
|
||||
### `useACLRoleContext()`
|
||||
|
||||
### `useRoleRecheck()`
|
@ -1,25 +0,0 @@
|
||||
# BlockProvider
|
||||
|
||||
## Kernel Methods
|
||||
|
||||
### `<BlockProvider />`
|
||||
|
||||
### `useBlockRequestContext()`
|
||||
|
||||
## Build-in BlockProvider Components
|
||||
|
||||
### `<CalendarBlockProvider />`
|
||||
|
||||
### `<TableFieldProvider />`
|
||||
|
||||
### `<TableBlockProvider />`
|
||||
|
||||
### `<TableSelectorProvider />`
|
||||
|
||||
### `<FormBlockProvider />`
|
||||
|
||||
### `<FormFieldProvider />`
|
||||
|
||||
### `<DetailsBlockProvider />`
|
||||
|
||||
### `<KanbanBlockProvider />`
|
@ -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.
|
@ -1,14 +0,0 @@
|
||||
# 适配的 Schema 组件
|
||||
|
||||
## Common
|
||||
|
||||
- DndContext
|
||||
- SortableItem
|
||||
|
||||
## And Design
|
||||
|
||||
- Action
|
||||
- BlockItem
|
||||
- Calendar
|
||||
- CardItem
|
||||
- Cascader
|
@ -1,3 +0,0 @@
|
||||
# Overview
|
||||
|
||||
test
|
@ -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();
|
||||
```
|
@ -1,13 +0,0 @@
|
||||
# SchemaComponent
|
||||
|
||||
## Core Components
|
||||
|
||||
### `<SchemaComponentProvider />`
|
||||
### `<SchemaComponentOptions>`
|
||||
### `<SchemaComponent>`
|
||||
|
||||
## Core Methods
|
||||
|
||||
### `createDesignable()`
|
||||
### `useDesignable()`
|
||||
### `useCompile()`
|
@ -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
|
||||
},
|
||||
}
|
||||
```
|
@ -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 />`
|
@ -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();
|
||||
```
|
@ -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
|
||||
}
|
||||
]
|
||||
});
|
||||
```
|
@ -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,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
@ -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.
|
@ -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.
|
@ -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.
|
@ -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
|
||||
```
|
@ -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 值
|
@ -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.
|
@ -1,8 +0,0 @@
|
||||
**参数**
|
||||
|
||||
| 参数名 | 类型 | 默认值 | 描述 |
|
||||
| --- | --- | --- | --- |
|
||||
| `options.values` | `M` | `{}` | 插入的数据对象 |
|
||||
| `options.whitelist?` | `string[]` | - | `values` 字段的白名单,只有名单内的字段会被存储 |
|
||||
| `options.blacklist?` | `string[]` | - | `values` 字段的黑名单,名单内的字段不会被存储 |
|
||||
| `options.transaction?` | `Transaction` | - | 事务 |
|
@ -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`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
|
@ -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`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
|
@ -1,8 +0,0 @@
|
||||
**类型**
|
||||
```typescript
|
||||
type FindOneOptions = Omit<FindOptions, 'limit'>;
|
||||
```
|
||||
|
||||
**参数**
|
||||
|
||||
大部分参数与 `find()` 相同,不同之处在于 `findOne()` 只返回单条数据,所以不需要 `limit` 参数,且查询时始终为 `1`。
|
@ -1 +0,0 @@
|
||||
* `transaction`: 事务对象。如果没有传入事务参数,该方法会自动创建一个内部事务。
|
@ -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` 至少要传其一。
|
@ -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
|
||||
```
|
@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
@ -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>
|
||||
```
|
@ -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 |
|
@ -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',
|
||||
});
|
||||
```
|
@ -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());
|
||||
```
|
@ -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';
|
||||
}));
|
||||
```
|
@ -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 defining,see 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]
|
||||
```
|
@ -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>();
|
||||
```
|
@ -1 +0,0 @@
|
||||
# AppManager
|
@ -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'`
|
@ -1 +0,0 @@
|
||||
# I18n
|
@ -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()`
|
@ -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.
|
@ -1 +0,0 @@
|
||||
# Components
|
@ -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.
|
@ -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 />
|
||||
);
|
||||
}
|
||||
```
|
@ -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.
|
Before Width: | Height: | Size: 13 KiB |
@ -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.
|
Before Width: | Height: | Size: 131 KiB |
@ -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();
|
||||
});
|
||||
});
|
||||
```
|
@ -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
|
@ -1,2 +0,0 @@
|
||||
# ACL
|
||||
|
@ -1,2 +0,0 @@
|
||||
# 区块
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
```
|
@ -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
|
||||
- ActionBar:For 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
|
@ -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>
|
||||
);
|
||||
};
|
@ -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
|
Before Width: | Height: | Size: 1.0 MiB |
@ -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>
|
||||
);
|
||||
};
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
# Overview
|
||||
|
||||
<code src="./demo1.tsx"></code>
|
@ -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>
|
||||
);
|
||||
}
|
||||
```
|
@ -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>
|
||||
```
|
@ -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>
|
||||
);
|
||||
});
|
||||
```
|
@ -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)
|
||||
}
|
||||
}
|
||||
```
|
@ -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'
|
||||
```
|
@ -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
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 238 KiB |
@ -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
|
@ -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;
|
||||
```
|
@ -1 +0,0 @@
|
||||
# Building
|
@ -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) 中,可以直接在本地运行,查看效果。
|
@ -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
|
||||
};
|
||||
})
|
||||
```
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
],
|
||||
}
|
||||
```
|
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 8.2 KiB |
@ -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',
|
||||
});
|
||||
```
|
@ -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>
|
||||
```
|
||||
|
@ -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)
|
Before Width: | Height: | Size: 209 KiB |
@ -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)
|
@ -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
|
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 271 KiB |
@ -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.
|
@ -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.
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|