cabloy 5.1.163 → 5.1.164

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 (96) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +2 -0
  4. package/package.json +1 -1
  5. package/repo-docs/backend/shared-rbac-architecture.md +4 -0
  6. package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
  7. package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
  8. package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
  9. package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
  10. package/repo-docs/blogs/index.md +15 -0
  11. package/repo-docs/editions/overview.md +2 -0
  12. package/repo-docs/frontend/environment-config-guide.md +43 -10
  13. package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
  14. package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
  15. package/repo-docs/frontend/table-guide.md +42 -0
  16. package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
  17. package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
  18. package/repo-e2e/specs/cabloy-basic.spec.ts +246 -1
  19. package/vona/packages-cli/cli/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
  21. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  22. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  23. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  24. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
  25. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
  27. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
  28. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
  31. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
  32. package/vona/packages-cli/cli-set-api/package.json +1 -1
  33. package/vona/packages-vona/vona/package.json +1 -1
  34. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
  35. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  36. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  37. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
  38. package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
  39. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
  40. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
  41. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
  42. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  43. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
  44. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
  45. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
  46. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
  47. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
  48. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
  49. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
  50. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
  51. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
  52. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  53. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  54. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  55. package/zova/packages-cli/cli/package.json +2 -2
  56. package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
  57. package/zova/packages-cli/cli-set-front/package.json +1 -1
  58. package/zova/packages-zova/zova/package.json +2 -2
  59. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
  60. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
  61. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
  62. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
  63. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
  64. package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
  65. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
  66. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
  67. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
  68. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
  69. package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
  70. package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
  71. package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
  72. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
  73. package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
  74. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
  75. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
  76. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
  77. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
  78. package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
  79. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  80. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
  81. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
  82. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
  83. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  84. package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
  85. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
  86. package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
  87. package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
  88. package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
  89. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  90. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +3 -0
  91. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
  92. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  93. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
  94. package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
  95. package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
  96. package/zova/src/suite-vendor/a-zova/package.json +4 -4
@@ -40,7 +40,13 @@ export interface IDtoOptionsStudentSelectResItem extends IDecoratorDtoOptions {}
40
40
  ],
41
41
  }),
42
42
  ZovaRender.block('basic-page:blockToolbarBulk', {
43
- actions: [ZovaRender.tableActionBulk('basic-table:actionCreate')],
43
+ actions: [
44
+ ZovaRender.tableActionBulk('basic-table:actionCreate'),
45
+ ZovaRender.tableActionBulk('basic-table:actionDeleteBulk', {
46
+ requiresSelection: true,
47
+ selectedMaxIds: 100,
48
+ }),
49
+ ],
44
50
  }),
45
51
  ZovaRender.block('basic-page:blockTable'),
46
52
  ZovaRender.block('basic-page:blockPager'),
@@ -3,6 +3,7 @@ import type { IQueryParams } from 'vona-module-a-orm';
3
3
 
4
4
  import { BeanBase } from 'vona';
5
5
  import { Service } from 'vona-module-a-bean';
6
+ import { Core } from 'vona-module-a-core';
6
7
 
7
8
  import type { DtoStudentCreate } from '../dto/studentCreate.tsx';
8
9
  import type { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
@@ -86,6 +87,11 @@ export class ServiceStudent extends BeanBase {
86
87
  });
87
88
  }
88
89
 
90
+ @Core.transaction()
91
+ async deleteBulk(ids: TableIdentity[]): Promise<void> {
92
+ await this.scope.model.student.deleteBulk(ids, { include: getStudentRelationsInclude() });
93
+ }
94
+
89
95
  async deleteForce(id: TableIdentity) {
90
96
  return await this.scope.model.student.deleteById(id, {
91
97
  disableDeleted: true,
@@ -64,13 +64,47 @@ describe('student.test.ts', { concurrency: false }, () => {
64
64
  });
65
65
  });
66
66
 
67
+ it('action:student:bulkDeleteOpenapi', async () => {
68
+ await app.bean.executor.mockCtx(async () => {
69
+ const controller = app.bean.onion.controller
70
+ .getOnionsEnabledCached()
71
+ .find(item => item.beanOptions.beanFullName === 'training-student.controller.student')
72
+ ?.beanOptions.beanClass;
73
+ if (!controller) throw new Error('training-student.controller.student not found');
74
+ const apiJson = await app.bean.openapi.generateJsonOfControllerAction(
75
+ controller,
76
+ 'deleteBulk',
77
+ 'V31',
78
+ );
79
+ const operation = apiJson.paths?.['/api/training/student/bulk/delete']?.post;
80
+ assert.equal(operation?.operationId, 'TrainingStudent_deleteBulk');
81
+ assert.equal(operation?.summary?.toJSON(), 'Bulk Delete');
82
+ const requestSchema = operation?.requestBody?.content?.['application/json']?.schema as any;
83
+ const requestSchemaRef = requestSchema?.$ref;
84
+ assert.ok(requestSchemaRef);
85
+ const requestSchemaName = requestSchemaRef.replace('#/components/schemas/', '');
86
+ const component = apiJson.components?.schemas?.[requestSchemaName] as any;
87
+ assert.equal(component?.properties?.ids?.minItems, 1);
88
+ assert.equal(component?.properties?.ids?.maxItems, 100);
89
+ });
90
+ });
91
+
67
92
  it('action:student:filterFormLayoutMetadata', async () => {
68
93
  await app.bean.executor.mockCtx(async () => {
69
94
  const apiJson = await app.bean.openapi.generateJsonOfClass(DtoStudentSelectResItem);
70
95
  const component = Object.values(apiJson.components!.schemas as any).find(item => {
71
96
  return (item as any).properties?._operationsRow;
72
97
  });
73
- const filterBlock = (component as any)?.rest?.blocks?.[0]?.options?.blocks?.[0];
98
+ const pageBlocks = (component as any)?.rest?.blocks?.[0]?.options?.blocks;
99
+ const toolbarBlock = pageBlocks?.[1];
100
+ assert.equal(toolbarBlock?.render, 'basic-page:blockToolbarBulk');
101
+ const deleteBulkAction = toolbarBlock?.options?.actions?.find(
102
+ (action: any) => action.render === 'basic-table:actionDeleteBulk',
103
+ );
104
+ assert.equal(deleteBulkAction?.options?.requiresSelection, true);
105
+ assert.equal(deleteBulkAction?.options?.selectedMaxIds, 100);
106
+
107
+ const filterBlock = pageBlocks?.[0];
74
108
  assert.equal(filterBlock?.render, 'basic-page:blockFilter');
75
109
  assert.equal(filterBlock?.options?.formFieldLayout?.inline, true);
76
110
  assert.deepEqual(
@@ -449,6 +483,75 @@ describe('student.test.ts', { concurrency: false }, () => {
449
483
  });
450
484
  });
451
485
 
486
+ it('action:student:deleteBulk', async () => {
487
+ await app.bean.executor.mockCtx(async () => {
488
+ await app.bean.passport.signinMock();
489
+ const studentIds: string[] = [];
490
+ try {
491
+ for (const name of ['__DeleteBulkA__', '__DeleteBulkB__']) {
492
+ const studentId = await app.bean.executor.performAction('post', '/training/student', {
493
+ body: { name, mobile: `138${studentIds.length}1234567`, level: 1 },
494
+ });
495
+ studentIds.push(studentId);
496
+ }
497
+ const deleted = await app.bean.executor.performAction(
498
+ 'post',
499
+ '/training/student/bulk/delete',
500
+ { body: { ids: studentIds } },
501
+ );
502
+ assert.equal(deleted, null);
503
+ for (const studentId of studentIds) {
504
+ assert.equal(
505
+ await app.scope('training-student').model.student.getById(studentId),
506
+ undefined,
507
+ );
508
+ }
509
+ } finally {
510
+ for (const studentId of studentIds.reverse()) {
511
+ await app
512
+ .scope('training-student')
513
+ .model.studentContent.delete({ studentId }, { disableDeleted: true });
514
+ await app
515
+ .scope('training-student')
516
+ .model.student.deleteById(studentId, { disableDeleted: true });
517
+ }
518
+ await app.bean.passport.signout();
519
+ }
520
+ });
521
+ });
522
+
523
+ it('action:student:deleteBulk accepts duplicate and missing IDs', async () => {
524
+ await app.bean.executor.mockCtx(async () => {
525
+ await app.bean.passport.signinMock();
526
+ let studentId: string | undefined;
527
+ try {
528
+ studentId = await app.bean.executor.performAction('post', '/training/student', {
529
+ body: { name: '__DeleteBulkIdempotent__', mobile: '13891234567', level: 1 },
530
+ });
531
+ const deleted = await app.bean.executor.performAction(
532
+ 'post',
533
+ '/training/student/bulk/delete',
534
+ { body: { ids: [studentId, studentId, '__missing-student__'] } },
535
+ );
536
+ assert.equal(deleted, null);
537
+ assert.equal(
538
+ await app.scope('training-student').model.student.getById(studentId),
539
+ undefined,
540
+ );
541
+ } finally {
542
+ if (studentId) {
543
+ await app
544
+ .scope('training-student')
545
+ .model.studentContent.delete({ studentId }, { disableDeleted: true });
546
+ await app
547
+ .scope('training-student')
548
+ .model.student.deleteById(studentId, { disableDeleted: true });
549
+ }
550
+ await app.bean.passport.signout();
551
+ }
552
+ });
553
+ });
554
+
452
555
  it('action:student:contentPersistence', async () => {
453
556
  await app.bean.executor.mockCtx(async () => {
454
557
  await app.bean.passport.signinMock();
@@ -531,7 +634,16 @@ describe('student.test.ts', { concurrency: false }, () => {
531
634
  await app.bean.passport.signinMock();
532
635
  try {
533
636
  app.bean.passport.current!.roles = [];
534
- const actions = ['create', 'select', 'view', 'update', 'summary', 'delete', 'deleteForce'];
637
+ const actions = [
638
+ 'create',
639
+ 'select',
640
+ 'view',
641
+ 'update',
642
+ 'summary',
643
+ 'delete',
644
+ 'deleteBulk',
645
+ 'deleteForce',
646
+ ];
535
647
  const permissions = await Promise.all(
536
648
  actions.map(action =>
537
649
  app.bean.permission.retrievePermissionAction('training-student:student', action),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -27,6 +27,7 @@ declare module 'vona-module-a-rbac' {
27
27
  /** guard: end */
28
28
  /** bean: begin */
29
29
  export * from '../bean/bean.rbacCatalog.ts';
30
+ export * from '../bean/bean.rbacResourceBulk.ts';
30
31
  export * from '../bean/bean.rbacScope.ts';
31
32
 
32
33
  import 'vona';
@@ -41,6 +42,11 @@ declare module 'vona-module-a-rbac' {
41
42
  get scope(): ScopeModuleARbac;
42
43
  }
43
44
 
45
+ export interface BeanRbacResourceBulk {
46
+ /** @internal */
47
+ get scope(): ScopeModuleARbac;
48
+ }
49
+
44
50
  export interface BeanRbacScope {
45
51
  /** @internal */
46
52
  get scope(): ScopeModuleARbac;
@@ -49,11 +55,13 @@ declare module 'vona-module-a-rbac' {
49
55
  /** bean: end */
50
56
  /** bean: begin */
51
57
  import type { BeanRbacCatalog } from '../bean/bean.rbacCatalog.ts';
58
+ import type { BeanRbacResourceBulk } from '../bean/bean.rbacResourceBulk.ts';
52
59
  import type { BeanRbacScope } from '../bean/bean.rbacScope.ts';
53
60
  import 'vona';
54
61
  declare module 'vona' {
55
62
  export interface IBeanRecordGlobal {
56
63
  'rbacCatalog': BeanRbacCatalog;
64
+ 'rbacResourceBulk': BeanRbacResourceBulk;
57
65
  'rbacScope': BeanRbacScope;
58
66
  }
59
67
  }
@@ -139,8 +147,15 @@ declare module 'vona-module-a-rbac' {
139
147
  export * from '../config/config.ts';
140
148
  import type { config } from '../config/config.ts';
141
149
  /** config: end */
150
+ /** locale: begin */
151
+ import { locales } from './locales.ts';
152
+ /** locale: end */
153
+ /** error: begin */
154
+ export * from '../config/errors.ts';
155
+ import type { errors } from '../config/errors.ts';
156
+ /** error: end */
142
157
  /** scope: begin */
143
- import { BeanScopeBase, type BeanScopeUtil, type TypeModuleConfig } from 'vona';
158
+ import { BeanScopeBase, type BeanScopeUtil, type TypeModuleConfig, type TypeModuleErrors, type TypeModuleLocales, type TypeLocaleBase } from 'vona';
144
159
  import { Scope } from 'vona-module-a-bean';
145
160
 
146
161
  @Scope()
@@ -149,6 +164,8 @@ export class ScopeModuleARbac extends BeanScopeBase {}
149
164
  export interface ScopeModuleARbac {
150
165
  util: BeanScopeUtil;
151
166
  config: TypeModuleConfig<typeof config>;
167
+ error: TypeModuleErrors<typeof errors>;
168
+ locale: TypeModuleLocales<(typeof locales)[TypeLocaleBase]>;
152
169
  event: IModuleEvent;
153
170
  }
154
171
 
@@ -166,8 +183,12 @@ declare module 'vona' {
166
183
  'a-rbac': ReturnType<typeof config>;
167
184
  }
168
185
 
186
+ export interface IBeanScopeLocale {
187
+ 'a-rbac': (typeof locales)[TypeLocaleBase];
188
+ }
169
189
 
170
-
171
-
190
+ export interface IBeanScopeErrors {
191
+ 'a-rbac': typeof errors;
192
+ }
172
193
  }
173
194
  /** 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(`a-rbac::${key}`, ...args);
18
+ }
@@ -0,0 +1,50 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import { BeanBase } from 'vona';
4
+ import { Bean } from 'vona-module-a-bean';
5
+
6
+ import type { IRbacScopeAccess } from '../types/scope.ts';
7
+
8
+ export type TypeRbacResourceBulkLoad<Entry extends { id: TableIdentity }> = (
9
+ ids: readonly TableIdentity[],
10
+ ) => Promise<Entry[]>;
11
+
12
+ @Bean()
13
+ export class BeanRbacResourceBulk extends BeanBase {
14
+ async entries<Entry extends { id: TableIdentity }>(
15
+ ids: readonly TableIdentity[],
16
+ load: TypeRbacResourceBulkLoad<Entry>,
17
+ access: IRbacScopeAccess,
18
+ ): Promise<Entry[]> {
19
+ const requested = this.validateIds(ids);
20
+ const entries = await load(requested);
21
+ const entriesByKey = new Map(entries.map(entry => [this.idKey(entry.id), entry]));
22
+ if (
23
+ entries.length !== requested.length ||
24
+ requested.some(id => !entriesByKey.has(this.idKey(id)))
25
+ ) {
26
+ this.scope.error.ResourceBulkEntriesMissing.throw();
27
+ }
28
+ const orderedEntries = requested.map(id => entriesByKey.get(this.idKey(id))!);
29
+ access.checkEntries(orderedEntries);
30
+ return orderedEntries;
31
+ }
32
+
33
+ private validateIds(ids: readonly TableIdentity[]): TableIdentity[] {
34
+ if (ids.length === 0) this.scope.error.ResourceBulkIdsEmpty.throw();
35
+ const keys = new Set<string>();
36
+ for (const id of ids) {
37
+ const key = this.idKey(id);
38
+ if (keys.has(key)) this.scope.error.ResourceBulkIdsDuplicate.throw();
39
+ keys.add(key);
40
+ }
41
+ return [...ids];
42
+ }
43
+
44
+ private idKey(id: TableIdentity): string {
45
+ if (id === undefined || id === null || id === '') {
46
+ this.scope.error.ResourceBulkIdsInvalid.throw();
47
+ }
48
+ return String(id);
49
+ }
50
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './bean.rbacCatalog.ts';
2
+ export * from './bean.rbacResourceBulk.ts';
2
3
  export * from './bean.rbacScope.ts';
3
4
  export * from './event.policyInvalidated.ts';
4
5
  export * from './event.resolvePolicy.ts';
@@ -0,0 +1,18 @@
1
+ export const errors = {
2
+ ResourceBulkIdsEmpty: {
3
+ code: 1001,
4
+ status: 422,
5
+ },
6
+ ResourceBulkIdsInvalid: {
7
+ code: 1003,
8
+ status: 422,
9
+ },
10
+ ResourceBulkIdsDuplicate: {
11
+ code: 1004,
12
+ status: 422,
13
+ },
14
+ ResourceBulkEntriesMissing: {
15
+ code: 1005,
16
+ status: 404,
17
+ },
18
+ } as const;
@@ -0,0 +1,6 @@
1
+ export default {
2
+ ResourceBulkIdsEmpty: 'Bulk resource IDs are required',
3
+ ResourceBulkIdsInvalid: 'Bulk resource IDs cannot contain empty values',
4
+ ResourceBulkIdsDuplicate: 'Bulk resource IDs cannot contain duplicates',
5
+ ResourceBulkEntriesMissing: 'One or more bulk resource entries are absent',
6
+ };
@@ -0,0 +1,6 @@
1
+ export default {
2
+ ResourceBulkIdsEmpty: '批量资源 ID 不能为空',
3
+ ResourceBulkIdsInvalid: '批量资源 ID 不能包含空值',
4
+ ResourceBulkIdsDuplicate: '批量资源 ID 不能包含重复值',
5
+ ResourceBulkEntriesMissing: '一个或多个批量资源条目不存在',
6
+ };
@@ -1,3 +1,4 @@
1
+ export * from './.metadata/locales.ts';
1
2
  export * from './lib/index.ts';
2
3
  export * from './types/index.ts';
3
4
  export * from './.metadata/index.ts';
@@ -0,0 +1,128 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import assert from 'node:assert';
4
+ import { describe, it } from 'node:test';
5
+ import { app } from 'vona-mock';
6
+
7
+ import type { IRbacScopeAccess } from '../src/types/scope.ts';
8
+
9
+ function createAccess(
10
+ onCheck?: (entries: readonly { id: TableIdentity }[]) => void,
11
+ ): IRbacScopeAccess {
12
+ return {
13
+ action: {} as never,
14
+ decision: {} as never,
15
+ unrestricted: false,
16
+ ownerValues: () => ({}),
17
+ where: () => undefined,
18
+ checkEntry: () => {},
19
+ checkEntries: entries => onCheck?.(entries as readonly { id: TableIdentity }[]),
20
+ permissionProjection: () => ({ key: '', allowed: true, matcher: { mode: 'all' } }),
21
+ };
22
+ }
23
+
24
+ function assertScopedError(errorName: string, status: number) {
25
+ return (error: Error) => error.code === `a-rbac:${errorName}` && error.status === status;
26
+ }
27
+
28
+ describe('resourceBulk.test.ts', { concurrency: false }, () => {
29
+ it('rejects invalid IDs before loading with scoped errors', async () => {
30
+ await app.bean.executor.mockCtx(async () => {
31
+ let loads = 0;
32
+ const load = async () => {
33
+ loads += 1;
34
+ return [];
35
+ };
36
+ await assert.rejects(
37
+ () => app.bean.rbacResourceBulk.entries([], load, createAccess()),
38
+ assertScopedError('1001', 422),
39
+ );
40
+ await assert.rejects(
41
+ () => app.bean.rbacResourceBulk.entries([undefined as never], load, createAccess()),
42
+ assertScopedError('1003', 422),
43
+ );
44
+ await assert.rejects(
45
+ () => app.bean.rbacResourceBulk.entries(['1', '1'], load, createAccess()),
46
+ assertScopedError('1004', 422),
47
+ );
48
+ assert.equal(loads, 0);
49
+ });
50
+ });
51
+
52
+ it('requires full coverage and preserves requested order for authorization', async () => {
53
+ await app.bean.executor.mockCtx(async () => {
54
+ let checked: readonly { id: TableIdentity }[] | undefined;
55
+ const entries = await app.bean.rbacResourceBulk.entries(
56
+ ['2', '1'],
57
+ async () => [{ id: '1' }, { id: '2' }],
58
+ createAccess(value => {
59
+ checked = value;
60
+ }),
61
+ );
62
+ assert.deepEqual(
63
+ entries.map(entry => entry.id),
64
+ ['2', '1'],
65
+ );
66
+ assert.deepEqual(
67
+ checked?.map(entry => entry.id),
68
+ ['2', '1'],
69
+ );
70
+
71
+ await assert.rejects(
72
+ () =>
73
+ app.bean.rbacResourceBulk.entries(['1', '2'], async () => [{ id: '1' }], createAccess()),
74
+ assertScopedError('1005', 404),
75
+ );
76
+ });
77
+ });
78
+
79
+ it('runs authorization only after complete scoped loading', async () => {
80
+ await app.bean.executor.mockCtx(async () => {
81
+ let calls = 0;
82
+ let loads = 0;
83
+ await assert.rejects(
84
+ () =>
85
+ app.bean.rbacResourceBulk.entries(
86
+ ['1', '2'],
87
+ async () => {
88
+ loads += 1;
89
+ return [{ id: '1' }];
90
+ },
91
+ createAccess(() => {
92
+ calls += 1;
93
+ }),
94
+ ),
95
+ assertScopedError('1005', 404),
96
+ );
97
+ assert.equal(loads, 1);
98
+ assert.equal(calls, 0);
99
+
100
+ await assert.rejects(
101
+ () =>
102
+ app.bean.rbacResourceBulk.entries(
103
+ ['1'],
104
+ async () => [{ id: '1' }],
105
+ createAccess(() => {
106
+ throw new Error('forbidden');
107
+ }),
108
+ ),
109
+ /forbidden/,
110
+ );
111
+ });
112
+ });
113
+
114
+ it('localizes scoped errors', async () => {
115
+ await app.bean.executor.mockCtx(
116
+ async () => {
117
+ await assert.rejects(
118
+ () => app.bean.rbacResourceBulk.entries([], async () => [], createAccess()),
119
+ (error: Error) =>
120
+ error.code === 'a-rbac:1001' &&
121
+ error.status === 422 &&
122
+ error.message === '批量资源 ID 不能为空',
123
+ );
124
+ },
125
+ { locale: 'zh-cn' },
126
+ );
127
+ });
128
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.51",
3
+ "version": "5.1.52",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -9,7 +9,7 @@
9
9
  "dependencies": {
10
10
  "vona-module-a-cabloy": "workspace:^",
11
11
  "vona-module-a-markdown": "^5.0.3",
12
- "vona-module-a-rbac": "^5.0.5",
12
+ "vona-module-a-rbac": "^5.0.6",
13
13
  "vona-module-a-socket": "workspace:^",
14
14
  "vona-module-a-ssr": "workspace:^",
15
15
  "vona-module-a-ssrhmr": "workspace:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.21",
3
+ "version": "5.1.22",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.85",
3
+ "version": "5.1.86",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli",
3
- "version": "1.2.106",
3
+ "version": "1.2.107",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli",
6
6
  "keywords": [
@@ -44,7 +44,7 @@
44
44
  "@cabloy/process-helper": "^3.1.8",
45
45
  "fs-extra": "^11.3.5",
46
46
  "semver": "^7.6.2",
47
- "zova-cli-set-front": "^1.2.104"
47
+ "zova-cli-set-front": "^1.2.105"
48
48
  },
49
49
  "devDependencies": {
50
50
  "clean-package": "^2.2.0",
@@ -1,7 +1,7 @@
1
1
  import type { IComponentOptions } from 'zova';
2
2
  import type {
3
3
  IJsxRenderContextPage,
4
- IResourceTableActionBulkOptionsBase,
4
+ IResourceTableActionBulkPropsBase,
5
5
  } from 'zova-module-a-openapi';
6
6
 
7
7
  import { BeanControllerBase, Use } from 'zova';
@@ -13,7 +13,7 @@ declare module 'zova-module-a-openapi' {
13
13
  }
14
14
  }
15
15
 
16
- export interface Controller<%=argv.nameMeta.fullCapitalize%>Props extends IResourceTableActionBulkOptionsBase {}
16
+ export interface Controller<%=argv.nameMeta.fullCapitalize%>Props extends IResourceTableActionBulkPropsBase {}
17
17
 
18
18
  @Controller()
19
19
  export class Controller<%=argv.nameMeta.fullCapitalize%> extends BeanControllerBase {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-cli-set-front",
3
- "version": "1.2.104",
3
+ "version": "1.2.105",
4
4
  "gitHead": "6f675a8cc46d596142c591c28a40cc4d82fcc6cc",
5
5
  "description": "zova cli-set-front",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova",
3
- "version": "5.1.156",
3
+ "version": "5.1.157",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A vue3 framework with ioc",
6
6
  "keywords": [
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "zova-core": "^5.1.88",
49
- "zova-suite-a-zova": "^5.1.155"
49
+ "zova-suite-a-zova": "^5.1.156"
50
50
  },
51
51
  "devDependencies": {
52
52
  "clean-package": "^2.2.0",
@@ -366,10 +366,10 @@ export type ApiSchemaCommerceCatalogDtoProductPublicDetail_2d063d28bc7243bed02eb
366
366
  components['schemas']['commerce-catalog.dto.productPublicDetail_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
367
367
  export type ApiSchemaCommerceCatalogDtoProductPublicDetail_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
368
368
  Partial<ApiSchemaCommerceCatalogDtoProductPublicDetail_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
369
- export type ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_9a6ea970b2f00d574b88030d8c96cbf7b1e699d7 =
370
- components['schemas']['commerce-catalog.dto.productView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_9a6ea970b2f00d574b88030d8c96cbf7b1e699d7'];
371
- export type ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_9a6ea970b2f00d574b88030d8c96cbf7b1e699d7Partial =
372
- Partial<ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_9a6ea970b2f00d574b88030d8c96cbf7b1e699d7>;
369
+ export type ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_a2f92cabb46b0630848b50414c0d4c60efb47a57 =
370
+ components['schemas']['commerce-catalog.dto.productView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_a2f92cabb46b0630848b50414c0d4c60efb47a57'];
371
+ export type ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_a2f92cabb46b0630848b50414c0d4c60efb47a57Partial =
372
+ Partial<ApiSchemaCommerceCatalogDtoProductView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_a2f92cabb46b0630848b50414c0d4c60efb47a57>;
373
373
  export type ApiSchemaCommerceCatalogDtoProductUpdate =
374
374
  components['schemas']['commerce-catalog.dto.productUpdate'];
375
375
  export type ApiSchemaCommerceCatalogDtoProductUpdatePartial =
@@ -435,10 +435,10 @@ export type ApiSchemaAImageDtoImageView = components['schemas']['a-image.dto.ima
435
435
  export type ApiSchemaAImageDtoImageViewPartial = Partial<ApiSchemaAImageDtoImageView>;
436
436
  export type ApiSchemaAFileDtoFileView = components['schemas']['a-file.dto.fileView'];
437
437
  export type ApiSchemaAFileDtoFileViewPartial = Partial<ApiSchemaAFileDtoFileView>;
438
- export type ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875 =
439
- components['schemas']['training-record.dto.recordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875'];
440
- export type ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875Partial =
441
- Partial<ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_425dbecccd52e19e24888f99e1b1670233afa875>;
438
+ export type ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_7d715789d9643740a04e8a6c0f6fb951884bc347 =
439
+ components['schemas']['training-record.dto.recordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_7d715789d9643740a04e8a6c0f6fb951884bc347'];
440
+ export type ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_7d715789d9643740a04e8a6c0f6fb951884bc347Partial =
441
+ Partial<ApiSchemaTrainingRecordDtoRecordView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_7d715789d9643740a04e8a6c0f6fb951884bc347>;
442
442
  export type ApiSchemaTrainingRecordDtoRecordUpdate =
443
443
  components['schemas']['training-record.dto.recordUpdate'];
444
444
  export type ApiSchemaTrainingRecordDtoRecordUpdatePartial =
@@ -463,10 +463,10 @@ export type ApiSchemaAImageDtoImageView_a83c3e638bca4b30ec8675860cdc52d66f6a16d1
463
463
  components['schemas']['a-image.dto.imageView_a83c3e638bca4b30ec8675860cdc52d66f6a16d1_2d063d28bc7243bed02ebd8bddf1212a93c6305b_efb37794d7c03c65122279f90d79919f009c34e5_1816ff740d81c738ec055c7038bbd93beb9405a7_537cd6552a384183a9457fb6a920bbae337277f6'];
464
464
  export type ApiSchemaAImageDtoImageView_a83c3e638bca4b30ec8675860cdc52d66f6a16d1_2d063d28bc7243bed02ebd8bddf1212a93c6305b_efb37794d7c03c65122279f90d79919f009c34e5_1816ff740d81c738ec055c7038bbd93beb9405a7_537cd6552a384183a9457fb6a920bbae337277f6Partial =
465
465
  Partial<ApiSchemaAImageDtoImageView_a83c3e638bca4b30ec8675860cdc52d66f6a16d1_2d063d28bc7243bed02ebd8bddf1212a93c6305b_efb37794d7c03c65122279f90d79919f009c34e5_1816ff740d81c738ec055c7038bbd93beb9405a7_537cd6552a384183a9457fb6a920bbae337277f6>;
466
- export type ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_93b863fbe913f9389386c3f524075f08b39f48ac =
467
- components['schemas']['training-student.dto.studentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_93b863fbe913f9389386c3f524075f08b39f48ac'];
468
- export type ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_93b863fbe913f9389386c3f524075f08b39f48acPartial =
469
- Partial<ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_93b863fbe913f9389386c3f524075f08b39f48ac>;
466
+ export type ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_4826fc678e47603ec467782ddb0d02463792cd60 =
467
+ components['schemas']['training-student.dto.studentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_4826fc678e47603ec467782ddb0d02463792cd60'];
468
+ export type ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_4826fc678e47603ec467782ddb0d02463792cd60Partial =
469
+ Partial<ApiSchemaTrainingStudentDtoStudentView_2d063d28bc7243bed02ebd8bddf1212a93c6305b_4826fc678e47603ec467782ddb0d02463792cd60>;
470
470
  export type ApiSchemaTrainingStudentDtoStudentUpdate =
471
471
  components['schemas']['training-student.dto.studentUpdate'];
472
472
  export type ApiSchemaTrainingStudentDtoStudentUpdatePartial =
@@ -475,6 +475,10 @@ export type ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bdd
475
475
  components['schemas']['training-student.dto.studentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305b'];
476
476
  export type ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305bPartial =
477
477
  Partial<ApiSchemaTrainingStudentDtoStudentSummary_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
478
+ export type ApiSchemaTrainingStudentDtoStudentDeleteBulk =
479
+ components['schemas']['training-student.dto.studentDeleteBulk'];
480
+ export type ApiSchemaTrainingStudentDtoStudentDeleteBulkPartial =
481
+ Partial<ApiSchemaTrainingStudentDtoStudentDeleteBulk>;
478
482
  export type ApiSchemaBasicMetricsDtoMetricsSnapshot =
479
483
  components['schemas']['basic-metrics.dto.metricsSnapshot'];
480
484
  export type ApiSchemaBasicMetricsDtoMetricsSnapshotPartial =