cabloy 5.1.179 → 5.1.180

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/scheduled_tasks.lock +1 -1
  3. package/CHANGELOG.md +19 -0
  4. package/package.json +1 -1
  5. package/repo-docs/.vitepress/config.mjs +1 -0
  6. package/repo-docs/backend/controller-aop-guide.md +1 -1
  7. package/repo-docs/backend/demonstration-mode-guide.md +130 -0
  8. package/repo-docs/frontend/page-meta-guide.md +91 -13
  9. package/repo-docs/frontend/router-view-hosts-guide.md +37 -0
  10. package/repo-docs/fullstack/framework-performance.md +3 -3
  11. package/vona/packages-cli/cli/package.json +1 -1
  12. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +7 -7
  13. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/test/{{resourceName}}.test.ts_ +93 -39
  14. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +1 -1
  15. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +1 -1
  16. package/vona/packages-cli/cli-set-api/package.json +1 -1
  17. package/vona/packages-vona/vona/package.json +1 -1
  18. package/vona/pnpm-lock.yaml +1862 -2223
  19. package/vona/src/suite/a-demo/modules/demo-demonstration/package.json +55 -0
  20. package/vona/src/suite/a-demo/modules/demo-demonstration/src/.metadata/index.ts +68 -0
  21. package/vona/src/suite/a-demo/modules/demo-demonstration/src/.metadata/locales.ts +18 -0
  22. package/vona/src/suite/a-demo/modules/demo-demonstration/src/.metadata/this.ts +2 -0
  23. package/vona/src/suite/a-demo/modules/demo-demonstration/src/bean/guard.demonstration.ts +68 -0
  24. package/vona/src/suite/a-demo/modules/demo-demonstration/src/config/errors.ts +6 -0
  25. package/vona/src/suite/a-demo/modules/demo-demonstration/src/config/locale/en-us.ts +3 -0
  26. package/vona/src/suite/a-demo/modules/demo-demonstration/src/config/locale/zh-cn.ts +3 -0
  27. package/vona/src/suite/a-demo/modules/demo-demonstration/src/index.ts +3 -0
  28. package/vona/src/suite/a-demo/modules/demo-demonstration/src/types/index.ts +8 -0
  29. package/vona/src/suite/a-demo/modules/demo-demonstration/test/guardDemonstration.test.ts +121 -0
  30. package/vona/src/suite/a-demo/modules/demo-demonstration/tsconfig.build.json +11 -0
  31. package/vona/src/suite/a-demo/modules/demo-demonstration/tsconfig.json +7 -0
  32. package/vona/src/suite/a-demo/package.json +2 -1
  33. package/vona/src/suite/a-demo/tsconfig.json +3 -0
  34. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +9 -0
  35. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +9 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +19 -17
  37. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +47 -27
  38. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  39. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacScope.ts +13 -7
  40. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/rbacScopeCurrent.test.ts +28 -0
  41. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  42. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  43. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  44. package/zova/packages-utils/zova-jsx/package.json +2 -2
  45. package/zova/packages-zova/zova/package.json +3 -3
  46. package/zova/packages-zova/zova-core/package.json +1 -1
  47. package/zova/packages-zova/zova-core/src/bean/type.ts +1 -0
  48. package/zova/packages-zova/zova-core/src/composables/useController.ts +1 -1
  49. package/zova/src/suite/a-home/modules/home-indexadmin/src/page/dashboard/controller.tsx +26 -7
  50. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  51. package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/const.ts +1 -0
  52. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkey.ts +39 -2
  53. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
  54. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/pageHost.ts +9 -0
  55. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/router.ts +2 -0
  56. package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/pageHost.test.ts +53 -0
  57. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  58. package/zova/src/suite-vendor/a-zova/package.json +3 -3
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "vona-module-demo-demonstration",
3
+ "version": "5.0.0",
4
+ "description": "",
5
+ "keywords": [
6
+ "Vona Module"
7
+ ],
8
+ "author": "",
9
+ "files": [
10
+ "assets",
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": [
18
+ "./src/index.ts",
19
+ "./dist/index.d.ts"
20
+ ],
21
+ "import": "./src/index.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "scripts": {
27
+ "clean": "rimraf dist tsconfig.build.tsbuildinfo",
28
+ "tsc:publish": "npm run clean && node ../../../../../packages-cli/cli/src/bin/vona.ts :bin:buildModule --sourcemap && tsc -p tsconfig.build.json",
29
+ "prepublishOnly": "npm run tsc:publish",
30
+ "prepack": "clean-package",
31
+ "postpack": "clean-package restore"
32
+ },
33
+ "dependencies": {
34
+ "vona-module-a-aspect": "workspace:^",
35
+ "vona-module-a-user": "workspace:^"
36
+ },
37
+ "devDependencies": {
38
+ "clean-package": "^2.2.0",
39
+ "rimraf": "^6.1.3"
40
+ },
41
+ "clean-package": {
42
+ "indent": 2,
43
+ "remove": [
44
+ "clean-package",
45
+ "devDependencies",
46
+ "exports.\\..import"
47
+ ]
48
+ },
49
+ "title": "demo-demonstration",
50
+ "vonaModule": {
51
+ "dependencies": {
52
+ "a-vona": "5.0.0"
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,68 @@
1
+ // eslint-disable
2
+ /** guard: begin */
3
+ export * from '../bean/guard.demonstration.ts';
4
+ import type { IGuardOptionsDemonstration } from '../bean/guard.demonstration.ts';
5
+ import 'vona-module-a-aspect';
6
+ declare module 'vona-module-a-aspect' {
7
+
8
+ export interface IGuardRecordGlobal {
9
+ 'demo-demonstration:demonstration': IGuardOptionsDemonstration;
10
+ }
11
+
12
+
13
+ }
14
+ declare module 'vona-module-demo-demonstration' {
15
+
16
+ export interface GuardDemonstration {
17
+ /** @internal */
18
+ get scope(): ScopeModuleDemoDemonstration;
19
+ }
20
+
21
+ export interface GuardDemonstration {
22
+ get $beanFullName(): 'demo-demonstration.guard.demonstration';
23
+ get $onionName(): 'demo-demonstration:demonstration';
24
+ get $onionOptions(): IGuardOptionsDemonstration;
25
+ }
26
+ }
27
+ /** guard: end */
28
+ /** locale: begin */
29
+ import { locales } from './locales.ts';
30
+ /** locale: end */
31
+ /** error: begin */
32
+ export * from '../config/errors.ts';
33
+ import type { errors } from '../config/errors.ts';
34
+ /** error: end */
35
+ /** scope: begin */
36
+ import { BeanScopeBase, type BeanScopeUtil, type TypeModuleErrors, type TypeModuleLocales, type TypeLocaleBase } from 'vona';
37
+ import { Scope } from 'vona-module-a-bean';
38
+
39
+ @Scope()
40
+ export class ScopeModuleDemoDemonstration extends BeanScopeBase {}
41
+
42
+ export interface ScopeModuleDemoDemonstration {
43
+ util: BeanScopeUtil;
44
+ error: TypeModuleErrors<typeof errors>;
45
+ locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
46
+ }
47
+
48
+ import 'vona';
49
+ declare module 'vona' {
50
+ export interface IBeanScopeRecord {
51
+ 'demo-demonstration': ScopeModuleDemoDemonstration;
52
+ }
53
+
54
+ export interface IBeanScopeContainer {
55
+ demoDemonstration: ScopeModuleDemoDemonstration;
56
+ }
57
+
58
+
59
+
60
+ export interface IBeanScopeLocale {
61
+ 'demo-demonstration': (typeof locales)[TypeLocaleBase];
62
+ }
63
+
64
+ export interface IBeanScopeErrors {
65
+ 'demo-demonstration': typeof errors;
66
+ }
67
+ }
68
+ /** scope: end */
@@ -0,0 +1,18 @@
1
+ import type { TypeLocaleBase } from 'vona';
2
+
3
+ import { $makeLocaleMagic } from 'vona';
4
+
5
+ import locale_en_us from '../config/locale/en-us.ts';
6
+ import locale_zh_cn from '../config/locale/zh-cn.ts';
7
+
8
+ export const locales = {
9
+ 'en-us': locale_en_us,
10
+ 'zh-cn': locale_zh_cn,
11
+ };
12
+
13
+ export function $locale<K extends keyof (typeof locales)[TypeLocaleBase]>(
14
+ key: K,
15
+ ...args: any[]
16
+ ): any {
17
+ return $makeLocaleMagic(`demo-demonstration::${key}`, ...args);
18
+ }
@@ -0,0 +1,2 @@
1
+ export const __ThisModule__ = 'demo-demonstration';
2
+ export { ScopeModuleDemoDemonstration as ScopeModule } from './index.ts';
@@ -0,0 +1,68 @@
1
+ import type { Next } from 'vona';
2
+ import type { IDecoratorGuardOptionsGlobal, IGuardExecute } from 'vona-module-a-aspect';
3
+
4
+ import { BeanBase, Global, useApp } from 'vona';
5
+ import { Guard } from 'vona-module-a-aspect';
6
+
7
+ const app = useApp();
8
+
9
+ const methodsForWrite = ['POST', 'PATCH', 'DELETE', 'PUT'];
10
+
11
+ export function parseUsernameWhitelist(value: string | undefined): string[] {
12
+ if (!value) return [];
13
+ return [
14
+ ...new Set(
15
+ value
16
+ .split(',')
17
+ .map(item => item.trim())
18
+ .filter(Boolean),
19
+ ),
20
+ ];
21
+ }
22
+
23
+ export interface IGuardOptionsDemonstration extends IDecoratorGuardOptionsGlobal {
24
+ methodsForWrite: string[];
25
+ usernameWhitelist: string[];
26
+ }
27
+
28
+ @Guard<IGuardOptionsDemonstration>({
29
+ enable: app.meta.env.DEMONSTRATION_ENABLED === 'true',
30
+ methodsForWrite,
31
+ usernameWhitelist: parseUsernameWhitelist(app.meta.env.DEMONSTRATION_USERNAME_WHITELIST),
32
+ ignore: [
33
+ '/home/user/passport/logout',
34
+ '/home/user/passport/register',
35
+ '/home/user/passport/login',
36
+ '/home/user/passport/login/:module/:providerName/:clientName?',
37
+ '/home/user/passport/associate/:module/:providerName/:clientName?',
38
+ '/home/user/passport/migrate/:module/:providerName/:clientName?',
39
+ '/home/user/passport/refreshAuthToken',
40
+ '/home/user/passport/createPassportJwtFromOauthCode',
41
+ '/home/user/passport/createTempAuthToken',
42
+ '/auth/passport/callback',
43
+ ],
44
+ dependencies: 'a-user:passport',
45
+ })
46
+ @Global()
47
+ export class GuardDemonstration extends BeanBase implements IGuardExecute {
48
+ async execute(options: IGuardOptionsDemonstration, next: Next): Promise<boolean> {
49
+ const method = this.ctx.method.toUpperCase();
50
+ if (!options.methodsForWrite.some(item => item.toUpperCase() === method)) {
51
+ return next();
52
+ }
53
+
54
+ const user = this.bean.passport.currentUser;
55
+ if (
56
+ this.bean.passport.isAuthenticated &&
57
+ this.bean.passport.isAccountActive &&
58
+ user &&
59
+ !user.anonymous &&
60
+ options.usernameWhitelist.includes(user.name)
61
+ ) {
62
+ return next();
63
+ }
64
+
65
+ this.scope.error.WriteForbidden.throw();
66
+ return false;
67
+ }
68
+ }
@@ -0,0 +1,6 @@
1
+ export const errors = {
2
+ WriteForbidden: {
3
+ code: 1001,
4
+ status: 403,
5
+ },
6
+ } as const;
@@ -0,0 +1,3 @@
1
+ export default {
2
+ WriteForbidden: 'This operation is forbidden in demonstration mode',
3
+ };
@@ -0,0 +1,3 @@
1
+ export default {
2
+ WriteForbidden: '在演示模式下禁止此操作',
3
+ };
@@ -0,0 +1,3 @@
1
+ export * from './.metadata/locales.ts';
2
+ export * from './types/index.ts';
3
+ export * from './.metadata/index.ts';
@@ -0,0 +1,8 @@
1
+ declare module 'vona' {
2
+ export interface VonaConfigEnv {
3
+ DEMONSTRATION_ENABLED: string | undefined;
4
+ DEMONSTRATION_USERNAME_WHITELIST: string | undefined;
5
+ }
6
+ }
7
+
8
+ export {};
@@ -0,0 +1,121 @@
1
+ import { catchError } from '@cabloy/utils';
2
+ import assert from 'node:assert';
3
+ import { describe, it } from 'node:test';
4
+ import { app } from 'vona-mock';
5
+ import { GuardDemonstration, parseUsernameWhitelist } from 'vona-module-demo-demonstration';
6
+
7
+ const guardBeanName = 'demo-demonstration.guard.demonstration';
8
+ const businessRoute = {
9
+ controllerBeanFullName: 'test.controller.business',
10
+ action: 'write',
11
+ };
12
+
13
+ function guard() {
14
+ return app.bean._getBean(guardBeanName as never) as GuardDemonstration;
15
+ }
16
+
17
+ function setRequest(method: string, route = businessRoute) {
18
+ Object.defineProperty(app.ctx, 'method', {
19
+ configurable: true,
20
+ value: method,
21
+ });
22
+ app.ctx.route = route as any;
23
+ app.ctx.innerAccess = false;
24
+ }
25
+
26
+ function isGuardEnabledForRoute(routePathRaw: string, enable = true): boolean {
27
+ const options = app.bean.onion.guard.getOnionOptions('demo-demonstration:demonstration');
28
+ assert.ok(options);
29
+ return app.bean.onion.checkOnionOptionsEnabled({ ...options, enable }, routePathRaw);
30
+ }
31
+
32
+ describe('guardDemonstration.test.ts', () => {
33
+ it('parses a trimmed, deduplicated username whitelist and exposes localized errors', async () => {
34
+ assert.deepEqual(parseUsernameWhitelist(' admin, demo, ,admin '), ['admin', 'demo']);
35
+ assert.deepEqual(parseUsernameWhitelist(undefined), []);
36
+ assert.deepEqual(parseUsernameWhitelist(''), []);
37
+
38
+ await app.bean.executor.mockCtx(async () => {
39
+ const scopeDemonstration = app.scope('demo-demonstration');
40
+ assert.equal(
41
+ scopeDemonstration.locale.WriteForbidden.locale('zh-cn'),
42
+ '在演示模式下禁止此操作',
43
+ );
44
+ assert.equal(
45
+ scopeDemonstration.locale.WriteForbidden.locale('en-us'),
46
+ 'This operation is forbidden in demonstration mode',
47
+ );
48
+ });
49
+ });
50
+
51
+ it('does not select disabled guards and allows read methods', async () => {
52
+ assert.equal(isGuardEnabledForRoute('/test/business/write', false), false);
53
+
54
+ await app.bean.executor.mockCtx(async () => {
55
+ const bean = guard();
56
+ setRequest('GET');
57
+ assert.equal(
58
+ await bean.execute(
59
+ { enable: true, methodsForWrite: ['POST'], usernameWhitelist: [] } as any,
60
+ async () => true,
61
+ ),
62
+ true,
63
+ );
64
+ });
65
+ });
66
+
67
+ it('rejects non-whitelisted writes with the module error', async () => {
68
+ await app.bean.executor.mockCtx(async () => {
69
+ await app.bean.passport.signinMock('admin');
70
+ try {
71
+ setRequest('POST');
72
+ const [_, err] = await catchError(() =>
73
+ guard().execute(
74
+ { enable: true, methodsForWrite: ['POST'], usernameWhitelist: [] } as any,
75
+ async () => true,
76
+ ),
77
+ );
78
+ assert.equal(err?.code, 'demo-demonstration:1001');
79
+ assert.equal(err?.status, 403);
80
+ } finally {
81
+ await app.bean.passport.signout();
82
+ }
83
+ });
84
+ });
85
+
86
+ it('allows an authenticated whitelisted user to write', async () => {
87
+ await app.bean.executor.mockCtx(async () => {
88
+ await app.bean.passport.signinMock('admin');
89
+ try {
90
+ setRequest('DELETE');
91
+ assert.equal(
92
+ await guard().execute(
93
+ { enable: true, methodsForWrite: ['DELETE'], usernameWhitelist: ['admin'] } as any,
94
+ async () => true,
95
+ ),
96
+ true,
97
+ );
98
+ } finally {
99
+ await app.bean.passport.signout();
100
+ }
101
+ });
102
+ });
103
+
104
+ it('excludes only the configured authentication and OAuth route paths', () => {
105
+ for (const routePathRaw of [
106
+ '/home/user/passport/login',
107
+ '/home/user/passport/login/:module/:providerName/:clientName?',
108
+ '/home/user/passport/logout',
109
+ '/home/user/passport/register',
110
+ '/home/user/passport/associate/:module/:providerName/:clientName?',
111
+ '/home/user/passport/migrate/:module/:providerName/:clientName?',
112
+ '/home/user/passport/refreshAuthToken',
113
+ '/home/user/passport/createPassportJwtFromOauthCode',
114
+ '/home/user/passport/createTempAuthToken',
115
+ '/auth/passport/callback',
116
+ ]) {
117
+ assert.equal(isGuardEnabledForRoute(routePathRaw), false);
118
+ }
119
+ assert.equal(isGuardEnabledForRoute('/test/business/write'), true);
120
+ });
121
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "emitDeclarationOnly": true,
8
+ "outDir": "dist"
9
+ },
10
+ "include": ["src", "src/.metadata/*.ts", "src/**/*.json"]
11
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base",
3
+ "compilerOptions": {
4
+ "noEmit": true
5
+ },
6
+ "include": ["src", "src/.metadata/*.ts", "src/**/*.json", "test"]
7
+ }
@@ -6,7 +6,8 @@
6
6
  "files": [],
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "vona-module-demo-basic": "^5.0.0"
9
+ "vona-module-demo-basic": "^5.0.0",
10
+ "vona-module-demo-demonstration": "^5.0.0"
10
11
  },
11
12
  "title": "a-demo"
12
13
  }
@@ -4,6 +4,9 @@
4
4
  "references": [
5
5
  {
6
6
  "path": "modules/demo-basic/tsconfig.build.json"
7
+ },
8
+ {
9
+ "path": "modules/demo-demonstration/tsconfig.build.json"
7
10
  }
8
11
  ],
9
12
  "exclude": ["modules"]
@@ -19,6 +19,15 @@ export default {
19
19
  StudentImage: 'Student Image',
20
20
  DossierFiles: 'Dossier Files',
21
21
  Summary: 'Summary',
22
+ StudentController: 'Student Management',
23
+ StudentCreate: 'Create Student',
24
+ StudentSelect: 'List Students',
25
+ StudentView: 'View Student',
26
+ StudentUpdate: 'Update Student',
27
+ StudentSummary: 'View Student Summary',
28
+ StudentDelete: 'Delete Student',
29
+ StudentDeleteForce: 'Permanently Delete Student',
30
+ StudentDeleteBulk: 'Bulk Delete Students',
22
31
  LevelTitle: 'Level Title',
23
32
  DescriptionLength: 'Description Length',
24
33
  ForceDelete: 'Force Delete',
@@ -19,6 +19,15 @@ export default {
19
19
  StudentImage: '学生图片',
20
20
  DossierFiles: '培训档案附件',
21
21
  Summary: '摘要',
22
+ StudentController: '学生管理',
23
+ StudentCreate: '创建学生',
24
+ StudentSelect: '查询学生列表',
25
+ StudentView: '查看学生',
26
+ StudentUpdate: '更新学生',
27
+ StudentSummary: '查看学生摘要',
28
+ StudentDelete: '删除学生',
29
+ StudentDeleteForce: '永久删除学生',
30
+ StudentDeleteBulk: '批量删除学生',
22
31
  LevelTitle: '阶段名称',
23
32
  DescriptionLength: '描述长度',
24
33
  ForceDelete: '强制删除',
@@ -22,39 +22,41 @@ import { DtoStudentView } from '../dto/studentView.tsx';
22
22
 
23
23
  export interface IControllerOptionsStudent extends IDecoratorControllerOptions {}
24
24
 
25
- @Controller<IControllerOptionsStudent>('student')
25
+ @Controller<IControllerOptionsStudent>('student', {
26
+ summary: $locale('StudentController'),
27
+ })
26
28
  @Resource()
27
29
  export class ControllerStudent extends BeanBase {
28
- @Web.post()
30
+ @Web.post('', { summary: $locale('StudentCreate') })
29
31
  @Api.body(v.tableIdentity())
30
- @Passport.systemAdmin()
32
+ @Passport.rbac()
31
33
  async create(@Arg.body() student: DtoStudentCreate): Promise<TableIdentity> {
32
34
  return (await this.scope.service.student.create(student)).id;
33
35
  }
34
36
 
35
- @Web.get()
37
+ @Web.get('', { summary: $locale('StudentSelect') })
36
38
  @Api.body(DtoStudentSelectRes)
37
39
  @Core.serializer()
38
- @Passport.systemAdmin()
40
+ @Passport.rbac()
39
41
  async select(
40
42
  @Arg.filter(DtoStudentSelectReq) params: IQueryParams<ModelStudent>,
41
43
  ): Promise<DtoStudentSelectRes> {
42
44
  return await this.scope.service.student.select(params);
43
45
  }
44
46
 
45
- @Web.get(':id')
47
+ @Web.get(':id', { summary: $locale('StudentView') })
46
48
  @Api.body(v.optional(), v.object(DtoStudentView))
47
49
  @Core.serializer()
48
- @Passport.systemAdmin()
50
+ @Passport.rbac()
49
51
  async view(
50
52
  @Arg.param('id', v.tableIdentity()) id: TableIdentity,
51
53
  ): Promise<DtoStudentView | undefined> {
52
54
  return await this.scope.service.student.view(id);
53
55
  }
54
56
 
55
- @Web.patch(':id')
57
+ @Web.patch(':id', { summary: $locale('StudentUpdate') })
56
58
  @Api.body(z.null())
57
- @Passport.systemAdmin()
59
+ @Passport.rbac()
58
60
  async update(
59
61
  @Arg.param('id', v.tableIdentity()) id: TableIdentity,
60
62
  @Arg.body() student: DtoStudentUpdate,
@@ -62,33 +64,33 @@ export class ControllerStudent extends BeanBase {
62
64
  await this.scope.service.student.update(id, student);
63
65
  }
64
66
 
65
- @Web.get('summary/:id')
67
+ @Web.get('summary/:id', { summary: $locale('StudentSummary') })
66
68
  @Api.body(v.optional(), v.object(DtoStudentSummary))
67
69
  @Core.serializer()
68
- @Passport.systemAdmin()
70
+ @Passport.rbac({ actionInherit: 'view' })
69
71
  async summary(
70
72
  @Arg.param('id', v.tableIdentity()) id: TableIdentity,
71
73
  ): Promise<DtoStudentSummary | undefined> {
72
74
  return await this.scope.service.student.summary(id);
73
75
  }
74
76
 
75
- @Web.post('bulk/delete', { summary: $locale('BulkDelete') })
77
+ @Web.post('bulk/delete', { summary: $locale('StudentDeleteBulk') })
76
78
  @Api.body(z.null())
77
- @Passport.systemAdmin()
79
+ @Passport.rbac({ actionInherit: 'delete' })
78
80
  async deleteBulk(@Arg.body() command: DtoStudentDeleteBulk): Promise<void> {
79
81
  await this.scope.service.student.deleteBulk(command.ids);
80
82
  }
81
83
 
82
- @Web.delete(':id')
84
+ @Web.delete(':id', { summary: $locale('StudentDelete') })
83
85
  @Api.body(z.null())
84
- @Passport.systemAdmin()
86
+ @Passport.rbac()
85
87
  async delete(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<void> {
86
88
  await this.scope.service.student.delete(id);
87
89
  }
88
90
 
89
- @Web.delete('deleteForce/:id')
91
+ @Web.delete('deleteForce/:id', { summary: $locale('StudentDeleteForce') })
90
92
  @Api.body(z.null())
91
- @Passport.systemAdmin()
93
+ @Passport.rbac({ actionInherit: 'delete' })
92
94
  async deleteForce(@Arg.param('id', v.tableIdentity()) id: TableIdentity): Promise<void> {
93
95
  await this.scope.service.student.deleteForce(id);
94
96
  }
@@ -2,7 +2,9 @@ import type { DtoStudentSelectRes, EntityStudent } from 'vona-module-training-st
2
2
 
3
3
  import assert from 'node:assert';
4
4
  import { describe, it } from 'node:test';
5
+ import { appMetadata } from 'vona';
5
6
  import { app } from 'vona-mock';
7
+ import { SymbolOpenApiOptions } from 'vona-module-a-openapiutils';
6
8
  import {
7
9
  DtoStudentCreate,
8
10
  DtoStudentSelectResItem,
@@ -64,13 +66,39 @@ describe('student.test.ts', { concurrency: false }, () => {
64
66
  });
65
67
  });
66
68
 
67
- it('action:student:bulkDeleteOpenapi', async () => {
69
+ it('action:student:openapiMetadata', async () => {
68
70
  await app.bean.executor.mockCtx(async () => {
69
71
  const controller = app.bean.onion.controller
70
72
  .getOnionsEnabledCached()
71
73
  .find(item => item.beanOptions.beanFullName === 'training-student.controller.student')
72
74
  ?.beanOptions.beanClass;
73
75
  if (!controller) throw new Error('training-student.controller.student not found');
76
+ const controllerMetadata = appMetadata.getMetadata<any>(SymbolOpenApiOptions, controller);
77
+ assert.equal(controllerMetadata?.summary?.toString(), 'training-student::StudentController');
78
+ const expectedSummaries = {
79
+ create: ['post', '/api/training/student', 'Create Student'],
80
+ select: ['get', '/api/training/student', 'List Students'],
81
+ view: ['get', '/api/training/student/{id}', 'View Student'],
82
+ update: ['patch', '/api/training/student/{id}', 'Update Student'],
83
+ summary: ['get', '/api/training/student/summary/{id}', 'View Student Summary'],
84
+ delete: ['delete', '/api/training/student/{id}', 'Delete Student'],
85
+ deleteForce: [
86
+ 'delete',
87
+ '/api/training/student/deleteForce/{id}',
88
+ 'Permanently Delete Student',
89
+ ],
90
+ deleteBulk: ['post', '/api/training/student/bulk/delete', 'Bulk Delete Students'],
91
+ } as const;
92
+ for (const [action, [method, path, summary]] of Object.entries(expectedSummaries)) {
93
+ const apiJson = await app.bean.openapi.generateJsonOfControllerAction(
94
+ controller,
95
+ action,
96
+ 'V31',
97
+ );
98
+ const operation = (apiJson.paths as any)?.[path]?.[method];
99
+ assert.ok(operation, `${action} operation is missing`);
100
+ assert.equal(operation.summary?.toJSON(), summary);
101
+ }
74
102
  const apiJson = await app.bean.openapi.generateJsonOfControllerAction(
75
103
  controller,
76
104
  'deleteBulk',
@@ -78,7 +106,6 @@ describe('student.test.ts', { concurrency: false }, () => {
78
106
  );
79
107
  const operation = apiJson.paths?.['/api/training/student/bulk/delete']?.post;
80
108
  assert.equal(operation?.operationId, 'TrainingStudent_deleteBulk');
81
- assert.equal(operation?.summary?.toJSON(), 'Bulk Delete');
82
109
  const requestSchema = operation?.requestBody?.content?.['application/json']?.schema as any;
83
110
  const requestSchemaRef = requestSchema?.$ref;
84
111
  assert.ok(requestSchemaRef);
@@ -639,32 +666,25 @@ describe('student.test.ts', { concurrency: false }, () => {
639
666
  });
640
667
  });
641
668
 
642
- it('action:student:systemAdmin', async () => {
669
+ it('action:student:rbacCatalog', async () => {
643
670
  await app.bean.executor.mockCtx(async () => {
644
- await app.bean.passport.signinMock();
645
- try {
646
- app.bean.passport.current!.roles = [];
647
- const actions = [
648
- 'create',
649
- 'select',
650
- 'view',
651
- 'update',
652
- 'summary',
653
- 'delete',
654
- 'deleteBulk',
655
- 'deleteForce',
656
- ];
657
- const permissions = await Promise.all(
658
- actions.map(action =>
659
- app.bean.permission.retrievePermissionAction('training-student:student', action),
660
- ),
661
- );
662
- assert.deepEqual(
663
- permissions,
664
- actions.map(() => false),
665
- );
666
- } finally {
667
- await app.bean.passport.signout();
671
+ const catalog = app.bean.rbacCatalog.getCatalog();
672
+ const controllerBeanFullName = 'training-student.controller.student';
673
+ const expectedInherits = {
674
+ create: undefined,
675
+ select: undefined,
676
+ view: undefined,
677
+ update: undefined,
678
+ summary: `${controllerBeanFullName}#view`,
679
+ delete: undefined,
680
+ deleteBulk: `${controllerBeanFullName}#delete`,
681
+ deleteForce: `${controllerBeanFullName}#delete`,
682
+ } as const;
683
+ for (const [action, actionInheritKey] of Object.entries(expectedInherits)) {
684
+ const descriptor = catalog.get(`${controllerBeanFullName}#${action}`);
685
+ assert.ok(descriptor, `${action} RBAC action is missing`);
686
+ assert.equal(descriptor.actionInheritKey, actionInheritKey);
687
+ assert.equal(descriptor.options.dataScope, undefined);
668
688
  }
669
689
  });
670
690
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"