diff --git a/examples/api-client/api.request.ts b/examples/api-client/api.request.ts new file mode 100644 index 000000000..6f529f758 --- /dev/null +++ b/examples/api-client/api.request.ts @@ -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); +})(); diff --git a/examples/api-client/api.resource.ts b/examples/api-client/api.resource.ts new file mode 100644 index 000000000..657c5ebb0 --- /dev/null +++ b/examples/api-client/api.resource.ts @@ -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); +})(); diff --git a/examples/api-client/server.ts b/examples/api-client/server.ts new file mode 100644 index 000000000..1116e3b8f --- /dev/null +++ b/examples/api-client/server.ts @@ -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; diff --git a/examples/app/__tests__/app.test.ts b/examples/app/__tests__/app.test.ts new file mode 100644 index 000000000..4f6abefbc --- /dev/null +++ b/examples/app/__tests__/app.test.ts @@ -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(); + }); +}); diff --git a/examples/app/acl.ts b/examples/app/acl.ts new file mode 100644 index 000000000..25d14470b --- /dev/null +++ b/examples/app/acl.ts @@ -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; diff --git a/examples/app/association2resource.ts b/examples/app/association2resource.ts new file mode 100644 index 000000000..9eb42caaa --- /dev/null +++ b/examples/app/association2resource.ts @@ -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; diff --git a/examples/app/collection2resource.ts b/examples/app/collection2resource.ts new file mode 100644 index 000000000..73aee6031 --- /dev/null +++ b/examples/app/collection2resource.ts @@ -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; diff --git a/examples/app/context/ctx.action.mergeParams.ts b/examples/app/context/ctx.action.mergeParams.ts new file mode 100644 index 000000000..bbc67efe1 --- /dev/null +++ b/examples/app/context/ctx.action.mergeParams.ts @@ -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; diff --git a/examples/app/context/ctx.action.ts b/examples/app/context/ctx.action.ts new file mode 100644 index 000000000..41d6c51db --- /dev/null +++ b/examples/app/context/ctx.action.ts @@ -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; diff --git a/examples/app/context/ctx.db.ts b/examples/app/context/ctx.db.ts new file mode 100644 index 000000000..705317490 --- /dev/null +++ b/examples/app/context/ctx.db.ts @@ -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; diff --git a/examples/app/context/ctx.i18n.ts b/examples/app/context/ctx.i18n.ts new file mode 100644 index 000000000..c3ad80a3b --- /dev/null +++ b/examples/app/context/ctx.i18n.ts @@ -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; diff --git a/examples/app/custom-command.ts b/examples/app/custom-command.ts new file mode 100644 index 000000000..a448eb667 --- /dev/null +++ b/examples/app/custom-command.ts @@ -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; + diff --git a/examples/app/custom-plugin.ts b/examples/app/custom-plugin.ts new file mode 100644 index 000000000..4dcc92fef --- /dev/null +++ b/examples/app/custom-plugin.ts @@ -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; diff --git a/examples/app/i18n.ts b/examples/app/i18n.ts new file mode 100644 index 000000000..7942c9490 --- /dev/null +++ b/examples/app/i18n.ts @@ -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; diff --git a/examples/app/middleware/acl.ts b/examples/app/middleware/acl.ts new file mode 100644 index 000000000..914d29f59 --- /dev/null +++ b/examples/app/middleware/acl.ts @@ -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=false,can.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; diff --git a/examples/app/middleware/app.ts b/examples/app/middleware/app.ts new file mode 100644 index 000000000..05f36df21 --- /dev/null +++ b/examples/app/middleware/app.ts @@ -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; diff --git a/examples/app/middleware/resourcer.ts b/examples/app/middleware/resourcer.ts new file mode 100644 index 000000000..b39fbf847 --- /dev/null +++ b/examples/app/middleware/resourcer.ts @@ -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; diff --git a/examples/app/migrations/add-migration.ts b/examples/app/migrations/add-migration.ts new file mode 100644 index 000000000..bc0ca91dc --- /dev/null +++ b/examples/app/migrations/add-migration.ts @@ -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; diff --git a/examples/app/multi-app.ts b/examples/app/multi-app.ts index ed66861cb..ea3e4fcc8 100644 --- a/examples/app/multi-app.ts +++ b/examples/app/multi-app.ts @@ -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 { IncomingMessage } from 'http'; @@ -20,10 +28,6 @@ const app = new Application({ plugins: [], }); -if (require.main === module) { - app.runAsCLI(); -} - const subApp1 = app.appManager.createApplication('sub1', { database: app.db, resourcer: { @@ -56,7 +60,8 @@ app.appManager.setAppSelector((req: IncomingMessage) => { return null; }); -export default app; +if (require.main === module) { + app.runAsCLI(); +} -// http://localhost:13000/api/test:list -// http://localhost:13000/sub1/api/test:list +export default app; diff --git a/examples/app/resource-actions/action-merge-params.ts b/examples/app/resource-actions/action-merge-params.ts new file mode 100644 index 000000000..2c0fc445e --- /dev/null +++ b/examples/app/resource-actions/action-merge-params.ts @@ -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', + }, +}); + +// 来源1:resourcer.use 中间件里直接 action.mergeParams +app.resourcer.use(async (ctx, next) => { + // 在 middleware 里修改 action.params + ctx.action.mergeParams({ + filter: { + col1: 'val1', + }, + fields: ['col1', 'col2', 'col4'], + }); + await next(); +}); + +// 来源2:app.acl.use 中间件里的 ctx.permission.can.params +app.acl.use(async (ctx, next) => { + ctx.permission = { + // 是否跳过 acl 判断 + skip: !!ctx.request.query.skip, + // 如果 skip=true 不处理 + // 如果 skip=false,can.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; diff --git a/examples/app/resource-actions/action-with-default-options.ts b/examples/app/resource-actions/action-with-default-options.ts new file mode 100644 index 000000000..6acd36a46 --- /dev/null +++ b/examples/app/resource-actions/action-with-default-options.ts @@ -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; diff --git a/examples/app/resource-actions/global-action.ts b/examples/app/resource-actions/global-action.ts new file mode 100644 index 000000000..f00e96e80 --- /dev/null +++ b/examples/app/resource-actions/global-action.ts @@ -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; diff --git a/examples/app/resource-actions/simple.ts b/examples/app/resource-actions/simple.ts new file mode 100644 index 000000000..8408c1f6f --- /dev/null +++ b/examples/app/resource-actions/simple.ts @@ -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; diff --git a/examples/app/single-app.ts b/examples/app/single-app.ts index 14911da0d..74079365e 100644 --- a/examples/app/single-app.ts +++ b/examples/app/single-app.ts @@ -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'; const app = new Application({ @@ -19,11 +30,13 @@ const app = new Application({ plugins: [], }); +// 定义了一个 test 资源,并提供了相对应的 list 方法 app.resource({ name: 'test', actions: { - async list(ctx) { + async list(ctx, next) { ctx.body = 'test list'; + await next(); }, }, }); @@ -33,5 +46,3 @@ if (require.main === module) { } export default app; - -// http://localhost:13000/api/test:list \ No newline at end of file diff --git a/examples/database/collections/tree/adjacency-list.ts b/examples/database/collections/tree/adjacency-list.ts new file mode 100644 index 000000000..152462202 --- /dev/null +++ b/examples/database/collections/tree/adjacency-list.ts @@ -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; diff --git a/examples/database/collections/tree/closure-table.ts b/examples/database/collections/tree/closure-table.ts new file mode 100644 index 000000000..f9956c7fa --- /dev/null +++ b/examples/database/collections/tree/closure-table.ts @@ -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; diff --git a/examples/database/collections/tree/materialized-path.ts b/examples/database/collections/tree/materialized-path.ts new file mode 100644 index 000000000..00c889359 --- /dev/null +++ b/examples/database/collections/tree/materialized-path.ts @@ -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; diff --git a/examples/database/collections/tree/nested-set.ts b/examples/database/collections/tree/nested-set.ts new file mode 100644 index 000000000..0289fc39d --- /dev/null +++ b/examples/database/collections/tree/nested-set.ts @@ -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; diff --git a/examples/index.md b/examples/index.md new file mode 100644 index 000000000..71de952f2 --- /dev/null +++ b/examples/index.md @@ -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) +- 客户端 SDK(APIClient)示例 + - [客户端常规请求 —— api.request()](./api-client/api.request.ts) + - [客户端资源请求 —— api.resource().action()](./api-client/api.resource.ts) + +## Database + +- 配置 Collections & Fields +- 通过 Repository 增删改查数据 +- 通过 Model 增删改查数据 +- 关系数据的增删改查 +- 关系数据的关联操作 +- 扩展字段 +- 数据库事件 +- 数据库迁移 + +## Client diff --git a/examples/index.ts b/examples/index.ts new file mode 100644 index 000000000..395e1c03e --- /dev/null +++ b/examples/index.ts @@ -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()); +} diff --git a/package.json b/package.json index 19de3e4eb..cd08733fb 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "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": "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": { "@types/react": "^17.0.0", diff --git a/packages/core/acl/src/acl.ts b/packages/core/acl/src/acl.ts index a604dce66..5260e617b 100644 --- a/packages/core/acl/src/acl.ts +++ b/packages/core/acl/src/acl.ts @@ -127,7 +127,7 @@ export class ACL extends EventEmitter { return this.configResources.includes(name); } - setAvailableAction(name: string, options: AvailableActionOptions) { + setAvailableAction(name: string, options: AvailableActionOptions = {}) { this.availableActions.set(name, new AclAvailableAction(name, options)); if (options.aliases) { diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 0a5108183..c035937e4 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -21,7 +21,7 @@ import { Collection, CollectionOptions, RepositoryType } from './collection'; import { ImporterReader, ImportFileExtension } from './collection-importer'; import * as FieldTypes from './fields'; import { Field, FieldContext, RelationField } from './fields'; -import { Migrations } from './migration'; +import { MigrationItem, Migrations } from './migration'; import { Model } from './model'; import { ModelHook } from './model-hook'; 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); } diff --git a/packages/core/database/src/index.ts b/packages/core/database/src/index.ts index 6b18d6ddf..811d04eda 100644 --- a/packages/core/database/src/index.ts +++ b/packages/core/database/src/index.ts @@ -1,4 +1,4 @@ -export { ModelCtor, Op, SyncOptions } from 'sequelize'; +export { DataTypes, ModelCtor, Op, SyncOptions } from 'sequelize'; export * from './collection'; export * from './database'; export { Database as default } from './database'; diff --git a/packages/core/resourcer/src/__tests__/koa.test.ts b/packages/core/resourcer/src/__tests__/koa.test.ts index 2cb2aaf58..1fc69ba87 100644 --- a/packages/core/resourcer/src/__tests__/koa.test.ts +++ b/packages/core/resourcer/src/__tests__/koa.test.ts @@ -206,7 +206,7 @@ describe('koa middleware', () => { sort: '-id', }); expect(response.body).toMatchObject({ - sort: '-id', + sort: ['-id'], filter: { $and: [ { col1: 'val1', col2: 'val2' }, diff --git a/packages/core/resourcer/src/assign.ts b/packages/core/resourcer/src/assign.ts index 6721b257e..fd78b19a8 100644 --- a/packages/core/resourcer/src/assign.ts +++ b/packages/core/resourcer/src/assign.ts @@ -32,6 +32,9 @@ function getKeys(target: any) { export const mergeStrategies = new Map(); mergeStrategies.set('overwrite', (_, y) => { + if (typeof y === 'string') { + y = y.split(','); + } return y; }); diff --git a/packages/core/server/src/__tests__/i18next.test.ts b/packages/core/server/src/__tests__/i18next.test.ts index addd1a8b8..8bf94c990 100644 --- a/packages/core/server/src/__tests__/i18next.test.ts +++ b/packages/core/server/src/__tests__/i18next.test.ts @@ -49,7 +49,7 @@ describe('i18next', () => { }); const response1 = await agent.get('/api/tests:get'); 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('你好'); const response3 = await agent.get('/api/tests:get?locale=zh-CN'); expect(response3.text).toEqual('你好'); diff --git a/packages/core/server/src/helper.ts b/packages/core/server/src/helper.ts index 170052f02..57c789115 100644 --- a/packages/core/server/src/helper.ts +++ b/packages/core/server/src/helper.ts @@ -55,7 +55,12 @@ export function registerMiddlewares(app: Application, options: ApplicationOption const i18n = app.i18n.cloneInstance({ initImmediate: false }); ctx.i18n = 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) { i18n.changeLanguage(lng); }