feat: add examples (#718)

* feat: add examples

* fix: tsconfig-paths/register

* feat: more examples

* fix: test errors

* feat: update examples

* docs: example list

* fix: updates

* feat: example for test case

* feat: api client

* fix: updates

* feat: tree structures

* fix: adjacency-list
This commit is contained in:
chenos 2022-08-16 14:41:29 +08:00 committed by GitHub
parent 7fde444a60
commit 28b73d7143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1623 additions and 18 deletions

View File

@ -0,0 +1,25 @@
/*
#
#
Step 1: 启动服务器
yarn run:example api-client/server start
Step 2: 客户端常规请求 api.request()
yarn run:example api-client/api.request
*/
import { APIClient } from '@nocobase/sdk';
const api = new APIClient({
baseURL: 'http://localhost:13000/api',
});
(async () => {
const response = await api.request({
url: 'test:list',
});
// 等价于
// const response = await api.resource('test').list();
console.log(response.data);
})();

View File

@ -0,0 +1,25 @@
/*
#
#
Step 1: 启动服务器
yarn run:example api-client/server start
Step 2: 客户端资源请求 api.resource(name).action(params)
yarn run:example api-client/api.resource
*/
import { APIClient } from '@nocobase/sdk';
const api = new APIClient({
baseURL: 'http://localhost:13000/api',
});
(async () => {
const response = await api.resource('test').list();
// 等价于
// const response = await api.request({
// url: 'test:list',
// });
console.log(response.data);
})();

View File

@ -0,0 +1,51 @@
/*
#
#
Step 1: 启动服务器
yarn run:example api-client/server start
Step 2: 客户端常规请求 api.request()
yarn run:example api-client/api.request
Step 3: 客户端资源请求 api.resource(name).action(params)
yarn run:example api-client/api.resource
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
// 定义了一个 test 资源,并提供了相对应的 list 方法
app.resource({
name: 'test',
actions: {
async list(ctx, next) {
ctx.body = 'test list';
await next();
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,33 @@
/*
# Application
#
yarn jest examples/app/__tests__/app.test.ts
*/
import { MockServer, mockServer } from '@nocobase/test';
describe('app test', () => {
let app: MockServer;
beforeEach(() => {
app = mockServer();
});
test('test1', async () => {
app.resource({
name: 'test',
actions: {
async list(ctx, next) {
ctx.body = 'test list';
await next();
},
},
});
const response = await app.agent().resource('test').list();
expect(response.body).toEqual({ data: 'test list' });
});
afterEach(async () => {
await app.destroy();
});
});

69
examples/app/acl.ts Normal file
View File

@ -0,0 +1,69 @@
/*
Step 1:
yarn run:example app/acl start
Step 2:
curl http://localhost:13000/api/test:export
curl --location --request GET 'http://localhost:13000/api/test:export' --header 'X-Role: admin'
curl --location --request GET 'http://localhost:13000/api/test:import' --header 'X-Role: admin'
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.acl.define({
role: 'admin',
actions: {
'test:export': {
fields: ['a1', 'b1']
},
},
});
app.resourcer.use(async (ctx, next) => {
ctx.state.currentRole = ctx.get('X-Role');
await next();
});
app.resourcer.use(app.acl.middleware());
app.resource({
name: 'test',
actions: {
async export(ctx, next) {
ctx.body = {
'ctx.action.params': ctx.action.params,
};
await next();
},
async import(ctx, next) {
ctx.body = {
'ctx.action.params': ctx.action.params,
};
await next();
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,91 @@
/*
Step 1: collections
yarn run:example app/association2resource db:sync
Step 2:
yarn run:example app/association2resource start
Step 3: Create article
curl --location --request POST 'http://localhost:13000/api/articles:create' \
--header 'Content-Type: application/json' \
--data-raw '{
"title": "My first article",
"content": "Hello NocoBase!",
"user": {
"name": "user 1"
}
}'
Step 4: View article user
curl http://localhost:13000/api/articles/1/user
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: 'a2r_',
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
// 已定义的 collection 会自动转为同名 resource
app.collection({
name: 'articles',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'text',
name: 'content',
},
// 自动转为 articles.user 资源
{
type: 'belongsTo',
name: 'user',
// 以下参数缺失时,自动处理
// target: 'users',
// foreignKey: 'userId',
// targetKey: 'id',
},
],
});
// 已定义的 collection 会自动转为同名 resource
app.collection({
name: 'users',
fields: [
{
type: 'string',
name: 'name',
},
// 自动转为 users.articles 资源
{
type: 'hasMany',
name: 'articles',
// 以下参数缺失时,自动处理
// target: 'articles',
// foreignKey: 'userId',
// sourceKey: 'id',
},
],
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,59 @@
/*
Step 1: collections
yarn run:example app/collection2resource db:sync
Step 2: 启动应用
yarn run:example app/collection2resource start
Step 3: Create article
curl --location --request POST 'http://localhost:13000/api/articles:create' \
--header 'Content-Type: application/json' \
--data-raw '{
"title": "My first article",
"content": "Hello NocoBase!"
}'
Step 4: View article list
curl http://localhost:13000/api/articles:list
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: 'c2r_',
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
// 已定义的 collection 会自动转为同名 resource
app.collection({
name: 'articles',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'text',
name: 'content',
},
],
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,77 @@
/*
# ctx.action.mergeParams
ctx.action.mergeParams()
#
Step 1:
yarn run:example app/context/ctx.action.mergeParams start
Step 2:
curl http://localhost:13000/api/test:list?filter%5Ba%5D=a2&fields=col1
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
});
async function list(ctx, next) {
ctx.body = {
'action.resourceName': ctx.action.resourceName,
'action.resourceOf': ctx.action.resourceOf,
'action.actionName': ctx.action.actionName,
'action.params': ctx.action.params,
};
await next();
}
async function get(ctx, next) {
ctx.body = {
'action.resourceName': ctx.action.resourceName,
'action.resourceOf': ctx.action.resourceOf,
'action.actionName': ctx.action.actionName,
'action.params': ctx.action.params,
};
await next();
}
app.resourcer.use(async (ctx, next) => {
// 在 middleware 里修改 action.params
ctx.action.mergeParams({
filter: {
a: 'a1',
},
fields: ['col1', 'col2'],
});
await next();
});
app.resource({
name: 'test',
actions: {
list,
get,
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,75 @@
/*
# ctx.action
#
Step 1:
yarn run:example app/context/ctx.action start
Step 2:
curl http://localhost:13000/api/test:list
curl http://localhost:13000/api/test/1/nest:get
curl http://localhost:13000/api/test:list?filter%5Ba%5D=a&fields=a,b&sort=a,b
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
});
async function list(ctx, next) {
ctx.body = {
'action.resourceName': ctx.action.resourceName,
'action.resourceOf': ctx.action.resourceOf,
'action.actionName': ctx.action.actionName,
'action.params': ctx.action.params,
};
await next();
}
async function get(ctx, next) {
ctx.body = {
'action.resourceName': ctx.action.resourceName,
'action.resourceOf': ctx.action.resourceOf,
'action.actionName': ctx.action.actionName,
'action.params': ctx.action.params,
};
await next();
}
app.resource({
name: 'test',
actions: {
list,
get,
},
});
app.resource({
name: 'test.nest',
actions: {
list,
get,
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,56 @@
/*
# ctx.db
#
Step 1:
yarn run:example app/context/ctx.db start
Step 2:
curl http://localhost:13000/
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.collection({
name: 'articles',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'text',
name: 'content',
},
],
});
app.use(async (ctx, next) => {
ctx.body = ctx.db.getCollection('articles').options;
await next();
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,75 @@
/*
#
app.i18n ctx.i18n
#
Step 1:
yarn run:example app/context/ctx.i18n start
Step 2:
curl http://localhost:13000/?locale=en-US
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
i18n: {
defaultNS: 'test',
resources: {
'en-US': {
test: {
hello: 'Hello',
},
},
'zh-CN': {
test: {
hello: '你好',
},
},
},
},
});
app.i18n.addResources('zh-CN', 'test', {
world: '世界',
});
app.i18n.addResources('en-US', 'test', {
world: 'World',
});
// 改变全局 app.i18n 的多语言,一般用于 cli 环境的多语言切换
app.i18n.changeLanguage('zh-CN');
app.use(async (ctx, next) => {
// ctx.i18n 是 app.i18n 的 clone instance
ctx.body = {
'ctx.i18n': ctx.i18n.language,
'app.i18n': app.i18n.language,
'ctx.t': ctx.t('hello') + ' ' + ctx.t('world'),
'app.i18n.t': app.i18n.t('hello') + ' ' + app.i18n.t('world'),
};
await next();
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,39 @@
/*
#
#
Step 1:
yarn run:example app/custom-command hello
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.command('hello').action(() => {
console.log('hello cli');
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,57 @@
/*
#
#
Step 1: Start app
yarn run:example plugins/custom-plugin start
Step 2: View test list
http://localhost:13000/api/test:list
*/
import { Application, Plugin } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
// Encapsulate modules into a plugin
class MyPlugin extends Plugin {
getName() {
return 'MyPlugin';
}
async load() {
app.resource({
name: 'test',
actions: {
async list(ctx) {
ctx.body = 'test list';
},
},
});
}
}
// Register plugin
app.plugin(MyPlugin);
if (require.main === module) {
app.runAsCLI();
}
export default app;

72
examples/app/i18n.ts Normal file
View File

@ -0,0 +1,72 @@
/*
#
Step 1:
yarn run:example app/i18n start
Step 2:
curl http://localhost:13000/api/test:get
curl http://localhost:13000/api/test:get?locale=en-US
curl --location --request GET 'http://localhost:13000/api/test:get' --header 'X-Locale: en-US'
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
i18n: {
defaultNS: 'test',
resources: {
'en-US': {
test: {
hello: 'Hello',
},
},
'zh-CN': {
test: {
hello: '你好',
},
},
},
},
});
app.i18n.addResources('zh-CN', 'test', {
world: '世界',
});
app.i18n.addResources('en-US', 'test', {
world: 'World',
});
// 改变全局 app.i18n 的多语言,一般用于 cli 环境的多语言
app.i18n.changeLanguage('zh-CN');
app.resource({
name: 'test',
actions: {
async get(ctx, next) {
ctx.body = ctx.t('hello') + ' ' + ctx.t('world');
await next();
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,87 @@
/*
# app.acl.use
#
Step 1:
yarn run:example app/middleware/acl start
Step 2:
curl http://localhost:13000/api/test:export
curl http://localhost:13000/api/test:export?skip=1
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.acl.define({
role: 'admin',
actions: {
'test:export': {
fields: ['a1', 'b1'],
},
},
});
app.acl.use(async (ctx, next) => {
ctx.permission = {
// 是否跳过 acl 判断
skip: !!ctx.request.query.skip,
// 如果 skip=true 不处理
// 如果 skip=falsecan.params 会通过 ctx.action.mergeParams() 合并到 ctx.action.params
can: {
params: {
fields: ['a1', 'b1', 'b3'],
},
},
};
// acl 中间件里也可以直接给 body 赋值
ctx.body = {
test: 'test',
};
await next();
});
app.resourcer.use(async (ctx, next) => {
// 当前角色
ctx.state.currentRole = ctx.get('X-Role');
await next();
});
app.resourcer.use(app.acl.middleware());
app.resource({
name: 'test',
actions: {
async export(ctx, next) {
ctx.body = {
...ctx.body,
'ctx.action.params': ctx.action.params,
};
await next();
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,43 @@
/*
# app.use Koa
#
Step 1:
yarn run:example app/middleware/app start
Step 2:
curl http://localhost:13000/
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
// Same as Koa
app.use(async (ctx, next) => {
ctx.body = 'Hello NocoBase';
await next();
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,60 @@
/*
# app.resourcer.use
#
Step 1:
yarn run:example app/middleware/resourcer start
Step 2:
curl http://localhost:13000/api/test:list
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.resource({
name: 'test',
actions: {
async list(ctx, next) {
ctx.body = ctx.body || [];
ctx.body.push('test list');
await next();
}
},
});
app.resourcer.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push('resourcer middleware');
await next();
});
app.use(async (ctx, next) => {
ctx.body = ctx.body || [];
ctx.body.push('app middleware');
await next();
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,62 @@
/*
# Application Migration
#
Step 1:
yarn run:example app/migrations/add-migration migrator up
Step 2:
yarn run:example app/migrations/add-migration migrator down
*/
import { DataTypes } from '@nocobase/database';
import { Application, Migration } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
class MyMigration extends Migration {
async up() {
/*
this.app;
this.db;
this.queryInterface;
this.sequelize;
*/
await this.queryInterface.createTable('test', {
name: DataTypes.STRING,
});
}
async down() {
await this.queryInterface.dropTable('test');
}
}
app.db.addMigration({
name: 'my-migration',
migration: MyMigration,
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -1,3 +1,11 @@
/*
#
yarn run:example app/multi-app start
curl http://localhost:13000/api/test:list
curl http://localhost:13000/sub1/api/test:list
*/
import { Application } from '@nocobase/server'; import { Application } from '@nocobase/server';
import { IncomingMessage } from 'http'; import { IncomingMessage } from 'http';
@ -20,10 +28,6 @@ const app = new Application({
plugins: [], plugins: [],
}); });
if (require.main === module) {
app.runAsCLI();
}
const subApp1 = app.appManager.createApplication('sub1', { const subApp1 = app.appManager.createApplication('sub1', {
database: app.db, database: app.db,
resourcer: { resourcer: {
@ -56,7 +60,8 @@ app.appManager.setAppSelector((req: IncomingMessage) => {
return null; return null;
}); });
export default app; if (require.main === module) {
app.runAsCLI();
}
// http://localhost:13000/api/test:list export default app;
// http://localhost:13000/sub1/api/test:list

View File

@ -0,0 +1,91 @@
/*
# Action
#
Step 1:
yarn run:example app/resource-actions/action-merge-params start
Step 2: 客户端请求时提供参数也是一种来源
curl http://localhost:13000/api/test:list?filter%5Ba%5D=a2&fields=col1
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
});
// 来源1resourcer.use 中间件里直接 action.mergeParams
app.resourcer.use(async (ctx, next) => {
// 在 middleware 里修改 action.params
ctx.action.mergeParams({
filter: {
col1: 'val1',
},
fields: ['col1', 'col2', 'col4'],
});
await next();
});
// 来源2app.acl.use 中间件里的 ctx.permission.can.params
app.acl.use(async (ctx, next) => {
ctx.permission = {
// 是否跳过 acl 判断
skip: !!ctx.request.query.skip,
// 如果 skip=true 不处理
// 如果 skip=falsecan.params 会通过 ctx.action.mergeParams() 合并到 ctx.action.params
can: {
params: {
filter: {
col1: 'val2',
},
fields: ['col1', 'col2', 'col3'],
},
},
};
await next();
});
app.resourcer.use(app.acl.middleware());
app.resource({
name: 'test',
actions: {
// 来源 3直接配置在 resource 的 action 里
list: {
filter: {
col1: 'val3',
},
fields: ['col1', 'col2', 'col3', 'col4', 'col5'],
handler: async (ctx, next) => {
ctx.body = {
'action.resourceName': ctx.action.resourceName,
'action.resourceOf': ctx.action.resourceOf,
'action.actionName': ctx.action.actionName,
'action.params': ctx.action.params,
};
await next();
},
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,54 @@
/*
# Action
#
Step 1:
yarn run:example app/resource-actions/action-with-default-options start
Step 2:
curl http://localhost:13000/api/test:list
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.resource({
name: 'test',
actions: {
find: {
filter: {
field1: 'value1',
},
handler: async (ctx, next) => {
ctx.body = {
'ctx.action.params': ctx.action.params,
};
await next();
},
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,68 @@
/*
# 使 Action
action resource
#
Step 1:
yarn run:example app/resource-actions/global-action start
Step 2: test:export action.params fields
curl http://localhost:13000/api/test:export
Step 3: test:import
curl http://localhost:13000/api/test:import
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.resourcer.registerActionHandlers({
async import(ctx, next) {
ctx.body = {
'ctx.action.params': ctx.action.params,
};
await next();
},
async export(ctx, next) {
ctx.body = {
'ctx.action.params': ctx.action.params,
};
await next();
},
});
app.resource({
name: 'test',
// 全局的 actions 如果有默认参数可以在 actions 里配置
actions: {
export: {
fields: ['field1', 'field2'],
},
// 如果没有默认参数,可以不配置,如 import action
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -0,0 +1,47 @@
/*
# resource actions
#
Step 1:
yarn run:example app/resource-actions/simple start
Step 2:
curl http://localhost:13000/api/test:list
*/
import { Application } from '@nocobase/server';
const app = new Application({
database: {
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: process.env.DB_TABLE_PREFIX,
},
resourcer: {
prefix: '/api',
},
plugins: [],
});
app.resource({
name: 'test',
actions: {
async list(ctx, next) {
ctx.body = 'test list';
await next();
},
},
});
if (require.main === module) {
app.runAsCLI();
}
export default app;

View File

@ -1,3 +1,14 @@
/*
#
#
Step 1:
yarn run:example app/single-app start
Step 2:
curl http://localhost:13000/api/test:list
*/
import { Application } from '@nocobase/server'; import { Application } from '@nocobase/server';
const app = new Application({ const app = new Application({
@ -19,11 +30,13 @@ const app = new Application({
plugins: [], plugins: [],
}); });
// 定义了一个 test 资源,并提供了相对应的 list 方法
app.resource({ app.resource({
name: 'test', name: 'test',
actions: { actions: {
async list(ctx) { async list(ctx, next) {
ctx.body = 'test list'; ctx.body = 'test list';
await next();
}, },
}, },
}); });
@ -33,5 +46,3 @@ if (require.main === module) {
} }
export default app; export default app;
// http://localhost:13000/api/test:list

View File

@ -0,0 +1,54 @@
/*
#
yarn run:example database/collections/tree/adjacency-list
*/
import { Database } from '@nocobase/database';
import { uid } from '@nocobase/utils';
const db = new Database({
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: `t_${uid()}_`,
});
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',
},
],
});
(async () => {
await db.sync();
await db.close();
})();
export default db;

View File

@ -0,0 +1,50 @@
/*
#
yarn run:example database/collections/tree/closure-table
*/
import { Database } from '@nocobase/database';
import { uid } from '@nocobase/utils';
const db = new Database({
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: `t_${uid()}_`,
});
db.collection({
name: 'categories',
tree: 'closure-table',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'description',
},
{
type: 'treeParent',
name: 'parent',
},
{
type: 'treeChildren',
name: 'children',
},
],
});
(async () => {
await db.sync();
await db.close();
})();
export default db;

View File

@ -0,0 +1,50 @@
/*
#
yarn run:example database/collections/tree/materialized-path
*/
import { Database } from '@nocobase/database';
import { uid } from '@nocobase/utils';
const db = new Database({
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: `t_${uid()}_`,
});
db.collection({
name: 'categories',
tree: 'materialized-path',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'description',
},
{
type: 'treeParent',
name: 'parent',
},
{
type: 'treeChildren',
name: 'children',
},
],
});
(async () => {
await db.sync();
await db.close();
})();
export default db;

View File

@ -0,0 +1,50 @@
/*
#
yarn run:example database/collections/tree/nested-set
*/
import { Database } from '@nocobase/database';
import { uid } from '@nocobase/utils';
const db = new Database({
logging: process.env.DB_LOGGING === 'on' ? console.log : false,
dialect: process.env.DB_DIALECT as any,
storage: process.env.DB_STORAGE,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
timezone: process.env.DB_TIMEZONE,
tablePrefix: `t_${uid()}_`,
});
db.collection({
name: 'categories',
tree: 'nested-set',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'description',
},
{
type: 'treeParent',
name: 'parent',
},
{
type: 'treeChildren',
name: 'children',
},
],
});
(async () => {
await db.sync();
await db.close();
})();
export default db;

47
examples/index.md Normal file
View File

@ -0,0 +1,47 @@
# 示例
## Application
- [最简单的单应用](./app/single-app.ts)
- [支持多应用(子应用)](./app/multi-app.ts)
- 配置 Resources 和 Actions
- [最简单的 resource actions](./app/resource-actions/simple.ts)
- [带默认参数的 Action](./app/resource-actions/action-with-default-options.ts)
- [使用全局 Action](./app/resource-actions/global-action.ts)
- [Action 参数的多来源合并](./app/resource-actions/action-merge-params.ts)
- 内置 Actions 的用法
- [Collection 自动转 Resource](./app/collection2resource.ts)
- [Association 自动转 Resource](./app/association2resource.ts)
- Context
- [ctx.db 示例](./app/context/ctx.db.ts)
- [ctx.action 的重要参数示例](./app/context/ctx.action.ts)
- [ctx.action.mergeParams() 示例](./app/context/ctx.action.mergeParams.ts)
- [ctx.i18n & ctx.t() 示例](./app/context/ctx.i18n.ts)
- 中间件
- [app.use 用法](./app/middleware/app.ts)
- [app.resourcer.use 用法](./app/middleware/resourcer.ts)
- [app.acl.use 用法](./app/middleware/acl.ts)
- [ACL](./app/acl.ts)
- [国际化多语言](./app/i18n.ts)
- [自定义命令行](./app/custom-command.ts)
- [编写一个最简单的插件](./app/custom-plugin.ts)
- Application Migration
- [编写一个新的 Migration 文件](./app/migrations/add-migration.ts)
- 编写 Application 测试用例
- [最简单的测试用例](./app/__tests__/app.test.ts)
- 客户端 SDKAPIClient示例
- [客户端常规请求 —— api.request()](./api-client/api.request.ts)
- [客户端资源请求 —— api.resource().action()](./api-client/api.resource.ts)
## Database
- 配置 Collections & Fields
- 通过 Repository 增删改查数据
- 通过 Model 增删改查数据
- 关系数据的增删改查
- 关系数据的关联操作
- 扩展字段
- 数据库事件
- 数据库迁移
## Client

14
examples/index.ts Normal file
View File

@ -0,0 +1,14 @@
import Database from '@nocobase/database';
import Application from '@nocobase/server';
const argv = process.argv;
const path = argv.splice(2, 1).shift();
const app = require(`./${path}`).default;
if (app instanceof Application) {
app.runAsCLI(argv);
}
if (app instanceof Database) {
console.log('Table prefix: ', app.getTablePrefix());
}

View File

@ -24,7 +24,7 @@
"version:alpha": "lerna version prerelease --preid alpha --force-publish=* --no-git-tag-version -m \"chore(versions): publish packages %s\"", "version:alpha": "lerna version prerelease --preid alpha --force-publish=* --no-git-tag-version -m \"chore(versions): publish packages %s\"",
"release:force": "lerna publish from-package --yes", "release:force": "lerna publish from-package --yes",
"release": "lerna publish", "release": "lerna publish",
"run:example": "ts-node-dev -r dotenv/config" "run:example": "ts-node-dev -r dotenv/config -r tsconfig-paths/register ./examples/index.ts"
}, },
"resolutions": { "resolutions": {
"@types/react": "^17.0.0", "@types/react": "^17.0.0",

View File

@ -127,7 +127,7 @@ export class ACL extends EventEmitter {
return this.configResources.includes(name); return this.configResources.includes(name);
} }
setAvailableAction(name: string, options: AvailableActionOptions) { setAvailableAction(name: string, options: AvailableActionOptions = {}) {
this.availableActions.set(name, new AclAvailableAction(name, options)); this.availableActions.set(name, new AclAvailableAction(name, options));
if (options.aliases) { if (options.aliases) {

View File

@ -21,7 +21,7 @@ import { Collection, CollectionOptions, RepositoryType } from './collection';
import { ImporterReader, ImportFileExtension } from './collection-importer'; import { ImporterReader, ImportFileExtension } from './collection-importer';
import * as FieldTypes from './fields'; import * as FieldTypes from './fields';
import { Field, FieldContext, RelationField } from './fields'; import { Field, FieldContext, RelationField } from './fields';
import { Migrations } from './migration'; import { MigrationItem, Migrations } from './migration';
import { Model } from './model'; import { Model } from './model';
import { ModelHook } from './model-hook'; import { ModelHook } from './model-hook';
import extendOperators from './operators'; import extendOperators from './operators';
@ -205,7 +205,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
}); });
} }
addMigration(item) { addMigration(item: MigrationItem) {
return this.migrations.add(item); return this.migrations.add(item);
} }

View File

@ -1,4 +1,4 @@
export { ModelCtor, Op, SyncOptions } from 'sequelize'; export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize';
export * from './collection'; export * from './collection';
export * from './database'; export * from './database';
export { Database as default } from './database'; export { Database as default } from './database';

View File

@ -206,7 +206,7 @@ describe('koa middleware', () => {
sort: '-id', sort: '-id',
}); });
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
sort: '-id', sort: ['-id'],
filter: { filter: {
$and: [ $and: [
{ col1: 'val1', col2: 'val2' }, { col1: 'val1', col2: 'val2' },

View File

@ -32,6 +32,9 @@ function getKeys(target: any) {
export const mergeStrategies = new Map<MergeStrategyType, MergeStrategyFunc>(); export const mergeStrategies = new Map<MergeStrategyType, MergeStrategyFunc>();
mergeStrategies.set('overwrite', (_, y) => { mergeStrategies.set('overwrite', (_, y) => {
if (typeof y === 'string') {
y = y.split(',');
}
return y; return y;
}); });

View File

@ -49,7 +49,7 @@ describe('i18next', () => {
}); });
const response1 = await agent.get('/api/tests:get'); const response1 = await agent.get('/api/tests:get');
expect(response1.text).toEqual('Hello'); expect(response1.text).toEqual('Hello');
const response2 = await agent.get('/api/tests:get').set('Accept-Language', 'zh-CN'); const response2 = await agent.get('/api/tests:get').set('X-Locale', 'zh-CN');
expect(response2.text).toEqual('你好'); expect(response2.text).toEqual('你好');
const response3 = await agent.get('/api/tests:get?locale=zh-CN'); const response3 = await agent.get('/api/tests:get?locale=zh-CN');
expect(response3.text).toEqual('你好'); expect(response3.text).toEqual('你好');

View File

@ -55,7 +55,12 @@ export function registerMiddlewares(app: Application, options: ApplicationOption
const i18n = app.i18n.cloneInstance({ initImmediate: false }); const i18n = app.i18n.cloneInstance({ initImmediate: false });
ctx.i18n = i18n; ctx.i18n = i18n;
ctx.t = i18n.t.bind(i18n); ctx.t = i18n.t.bind(i18n);
const lng = ctx.get('X-Locale') || (ctx.request.query.locale as string) || ctx.acceptsLanguages().shift() || 'en-US'; const lng =
ctx.get('X-Locale') ||
(ctx.request.query.locale as string) ||
app.i18n.language ||
ctx.acceptsLanguages().shift() ||
'en-US';
if (lng !== '*' && lng) { if (lng !== '*' && lng) {
i18n.changeLanguage(lng); i18n.changeLanguage(lng);
} }