cabloy 5.1.162 → 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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/package.json +1 -1
- package/repo-docs/backend/shared-rbac-architecture.md +4 -0
- package/repo-docs/blogs/ai-spec-driven-development/cover-en-v1.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-burndown.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/implementation-gantt.png +0 -0
- package/repo-docs/blogs/ai-spec-driven-development/index.md +217 -0
- package/repo-docs/blogs/index.md +15 -0
- package/repo-docs/editions/overview.md +2 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -10
- package/repo-docs/frontend/resource-list-page-deep-dive.md +14 -1
- package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +20 -0
- package/repo-docs/frontend/table-guide.md +42 -0
- package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -1
- package/repo-e2e/docs/specs/blogs-index.spec.ts +45 -9
- package/repo-e2e/specs/cabloy-basic.spec.ts +249 -2
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +17 -6
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +12 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +8 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}DeleteBulk.tsx_ +14 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectResItem.tsx_ +7 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +6 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +1 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +1 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +9 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentDeleteBulk.tsx +14 -0
- package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +7 -1
- package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +6 -0
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +114 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/index.ts +24 -3
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/.metadata/locales.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacResourceBulk.ts +50 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/errors.ts +18 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/en-us.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/config/locale/zh-cn.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/resourceBulk.test.ts +128 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/create/component/boilerplateTableActionBulk/controller.tsx_ +2 -2
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -12
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +66 -7
- package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +18 -0
- package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/.metadata/index.ts +16 -0
- package/zova/src/suite/cabloy-basic/modules/basic-commands/src/bean/command.deleteBulk.tsx +35 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +143 -7
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +3 -2
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockToolbarBulk/controller.tsx +76 -4
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/en-us.ts +7 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/config/locale/zh-cn.ts +6 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/lib/selection.ts +84 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/test/lib/selection.test.ts +99 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/component/actionDeleteBulk.ts +31 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/.metadata/index.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionCreate/controller.tsx +5 -2
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/actionDeleteBulk/controller.tsx +57 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +35 -7
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/en-us.ts +4 -0
- package/zova/src/suite/cabloy-basic/modules/basic-table/src/config/locale/zh-cn.ts +4 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/lib/selectedIds.ts +15 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +57 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/test/lib/selectedIds.test.ts +20 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-command/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/beanCommandSelectedBase.ts +33 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/src/types/command.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-command/test/lib/beanCommandSelectedBase.test.ts +47 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/action.ts +3 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/resource/tableActionBulk.ts +19 -0
- package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx +86 -26
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsx +30 -6
- package/zova/src/suite-vendor/a-zova/modules/a-table/src/types/table.ts +2 -0
- package/zova/src/suite-vendor/a-zova/package.json +4 -4
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
3
|
+
|
|
4
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
5
|
+
import { Dto } from 'vona-module-a-web';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export interface IDtoOptionsStudentDeleteBulk extends IDecoratorDtoOptions {}
|
|
9
|
+
|
|
10
|
+
@Dto<IDtoOptionsStudentDeleteBulk>()
|
|
11
|
+
export class DtoStudentDeleteBulk {
|
|
12
|
+
@Api.field(v.required(), z.array(v.tableIdentity()()).min(1).max(100))
|
|
13
|
+
ids: TableIdentity[];
|
|
14
|
+
}
|
|
@@ -40,7 +40,13 @@ export interface IDtoOptionsStudentSelectResItem extends IDecoratorDtoOptions {}
|
|
|
40
40
|
],
|
|
41
41
|
}),
|
|
42
42
|
ZovaRender.block('basic-page:blockToolbarBulk', {
|
|
43
|
-
actions: [
|
|
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
|
|
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 = [
|
|
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),
|
|
@@ -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
|
+
}
|
|
@@ -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,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.
|
|
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.
|
|
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": "zova-cli",
|
|
3
|
-
"version": "1.2.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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",
|
|
3
|
-
"version": "5.1.
|
|
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.
|
|
49
|
+
"zova-suite-a-zova": "^5.1.156"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"clean-package": "^2.2.0",
|