cabloy 5.1.249 → 5.1.251

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 (117) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/create-cabloy.yml +50 -3
  3. package/CHANGELOG.md +23 -0
  4. package/package.json +1 -1
  5. package/repo-agent-governance/managed-assets.json +12 -12
  6. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +1 -1
  7. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +1 -1
  8. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +2 -2
  9. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +1 -1
  10. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +73 -22
  11. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +98 -7
  12. package/repo-docs/ai/playbook-spec-execution.md +1 -1
  13. package/repo-docs/backend/crud-workflow.md +92 -1
  14. package/repo-docs/backend/dto-guide.md +16 -1
  15. package/repo-docs/backend/dto-infer-generation.md +23 -5
  16. package/repo-docs/backend/orm-aggregate-group-guide.md +32 -0
  17. package/repo-docs/backend/validation-guide.md +38 -2
  18. package/repo-e2e/specs/cabloy-basic.spec.ts +9 -2
  19. package/vona/packages-cli/cli/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/aggregate/src/dto/{{resourceName}}AggregateRes.tsx_ +11 -0
  21. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/common/src/lib/{{resourceName}}Analysis.ts_ +14 -0
  22. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/common/test/{{resourceName}}Analysis.test.ts_ +81 -0
  23. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudAnalysis/group/src/dto/{{resourceName}}GroupResItem.tsx_ +11 -0
  24. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  25. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  26. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  27. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/test/{{resourceName}}.test.ts_ +4 -0
  28. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/3-en-us.ts +2 -0
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +2 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  31. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  32. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  33. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/test/{{resourceName}}.test.ts_ +1 -1
  34. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/3-en-us.ts +2 -0
  35. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +2 -0
  36. package/vona/packages-cli/cli-set-api/package.json +1 -1
  37. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.crudBasic.ts +2 -0
  38. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.crudStart.ts +2 -0
  39. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.tools.masterDetail.ts +9 -1
  40. package/vona/packages-cli/cli-set-api/src/lib/crudAnalysis.ts +11 -0
  41. package/vona/packages-cli/cli-set-api/test/crudAnalysis.test.ts +385 -0
  42. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +1 -1
  43. package/vona/packages-vona/vona/package.json +1 -1
  44. package/vona/packages-vona/vona-core/package.json +1 -1
  45. package/vona/packages-vona/vona-mock/package.json +1 -1
  46. package/vona/pnpm-lock.yaml +143 -88
  47. package/vona/pnpm-workspace.yaml +1 -1
  48. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +18 -0
  49. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +2 -0
  50. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +2 -0
  51. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +28 -1
  52. package/vona/src/suite/a-training/modules/training-student/src/dto/studentAggregateRes.tsx +11 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/dto/studentGroupResItem.tsx +11 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectReq.tsx +2 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/lib/studentAnalysis.ts +12 -0
  56. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +22 -1
  57. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +4 -0
  58. package/vona/src/suite/a-training/modules/training-student/test/studentAnalysis.test.ts +422 -0
  59. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  60. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  61. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  62. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  64. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-locale/package.json +1 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  70. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/.metadata/index.ts +26 -26
  71. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/aggregateResult.ts +53 -0
  72. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/controls.ts +163 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/execute.ts +49 -0
  74. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/index.ts +3 -0
  75. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/policy.ts +140 -0
  76. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +4 -2
  77. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +8 -3
  78. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dto.ts +3 -0
  79. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAggregate.ts +4 -1
  80. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAnalysis.ts +34 -0
  81. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/index.ts +1 -0
  82. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/analysis.ts +137 -0
  83. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/index.ts +1 -0
  84. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsAggr.ts +1 -1
  85. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/aggregateResult.test.ts +81 -0
  86. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.test.ts +208 -0
  87. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.types.test.ts +112 -0
  88. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  92. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  93. package/vona/src/suite-vendor/a-vona/modules/a-web/src/.metadata/index.ts +40 -40
  94. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +44 -3
  95. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/analysisFilter.ts +69 -0
  96. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/arguments.ts +6 -4
  97. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/index.ts +1 -0
  98. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
  99. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/filter.ts +7 -2
  100. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  101. package/zova/packages-cli/cli/package.json +2 -2
  102. package/zova/packages-cli/cli-set-front/package.json +2 -2
  103. package/zova/packages-utils/zova-jsx/package.json +2 -2
  104. package/zova/packages-utils/zova-vite/package.json +1 -1
  105. package/zova/packages-zova/zova/package.json +3 -3
  106. package/zova/packages-zova/zova-core/package.json +1 -1
  107. package/zova/pnpm-lock.yaml +11 -11
  108. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -2
  109. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +201 -7
  110. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +42 -0
  111. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +10 -0
  112. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  113. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  114. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  115. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  116. package/zova/src/suite-vendor/a-zova/package.json +5 -5
  117. /package/vona/patches/{zova-core@5.1.95.patch → zova-core@5.1.96.patch} +0 -0
@@ -8,10 +8,12 @@ import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
8
 
9
9
  import { $locale } from '../.metadata/locales.ts';
10
10
  import { EntityStudent, studentLevelItems } from '../entity/student.tsx';
11
+ import { studentAnalysis } from '../lib/studentAnalysis.ts';
11
12
 
12
13
  export interface IDtoOptionsStudentSelectReq extends IDecoratorDtoOptions {}
13
14
 
14
15
  @Dto<IDtoOptionsStudentSelectReq>({
16
+ analysis: studentAnalysis,
15
17
  openapi: { filter: { table: 'trainingStudent' } },
16
18
  fields: {
17
19
  name: $makeSchema(v.optional(), z.string()),
@@ -0,0 +1,12 @@
1
+ import { $defineAnalysis } from 'vona-module-a-orm';
2
+
3
+ import { ModelStudent } from '../model/student.ts';
4
+
5
+ export const studentAnalysis = $defineAnalysis(() => ModelStudent, {
6
+ groups: ['level'],
7
+ aggrs: { count: ['*', 'imageId'] },
8
+ defaults: {
9
+ group: { groups: ['level'], aggrs: { count: ['*'] } },
10
+ aggregate: { aggrs: { count: ['*', 'imageId'] } },
11
+ },
12
+ });
@@ -1,11 +1,18 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams } from 'vona-module-a-orm';
2
+ import type {
3
+ IQueryParams,
4
+ TypeAnalysisAggregateParams,
5
+ TypeAnalysisGroupParams,
6
+ } from 'vona-module-a-orm';
3
7
 
4
8
  import { BeanBase } from 'vona';
5
9
  import { Service } from 'vona-module-a-bean';
6
10
  import { Core } from 'vona-module-a-core';
11
+ import { $executeAnalysisAggregate, $executeAnalysisGroup } from 'vona-module-a-orm';
7
12
 
13
+ import type { DtoStudentAggregateRes } from '../dto/studentAggregateRes.tsx';
8
14
  import type { DtoStudentCreate } from '../dto/studentCreate.tsx';
15
+ import type { DtoStudentGroupResItem } from '../dto/studentGroupResItem.tsx';
9
16
  import type { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
10
17
  import type { DtoStudentSummary } from '../dto/studentSummary.tsx';
11
18
  import type { DtoStudentUpdate } from '../dto/studentUpdate.tsx';
@@ -13,6 +20,8 @@ import type { DtoStudentView } from '../dto/studentView.tsx';
13
20
  import type { EntityStudent } from '../entity/student.tsx';
14
21
  import type { ModelStudent } from '../model/student.ts';
15
22
 
23
+ import { studentAnalysis } from '../lib/studentAnalysis.ts';
24
+
16
25
  type StudentContentInput = {
17
26
  descriptionMarkdown?: string;
18
27
  };
@@ -43,6 +52,18 @@ export class ServiceStudent extends BeanBase {
43
52
  return await this.scope.model.student.selectAndCount(params);
44
53
  }
45
54
 
55
+ async group(
56
+ params?: TypeAnalysisGroupParams<typeof studentAnalysis>,
57
+ ): Promise<DtoStudentGroupResItem[]> {
58
+ return await $executeAnalysisGroup(this.scope.model.student, studentAnalysis, params);
59
+ }
60
+
61
+ async aggregate(
62
+ params?: TypeAnalysisAggregateParams<typeof studentAnalysis>,
63
+ ): Promise<DtoStudentAggregateRes> {
64
+ return await $executeAnalysisAggregate(this.scope.model.student, studentAnalysis, params);
65
+ }
66
+
46
67
  async view(id: TableIdentity): Promise<DtoStudentView | undefined> {
47
68
  return await this.scope.model.student.getById(id, {
48
69
  include: getStudentRelationsInclude(),
@@ -78,6 +78,8 @@ describe('student.test.ts', { concurrency: false }, () => {
78
78
  const expectedSummaries = {
79
79
  create: ['post', '/api/training/student', 'Create Student'],
80
80
  select: ['get', '/api/training/student', 'List Students'],
81
+ group: ['get', '/api/training/student/group', 'Group Students'],
82
+ aggregate: ['get', '/api/training/student/aggregate', 'Aggregate Students'],
81
83
  view: ['get', '/api/training/student/{id}', 'View Student'],
82
84
  update: ['patch', '/api/training/student/{id}', 'Update Student'],
83
85
  summary: ['get', '/api/training/student/summary/{id}', 'View Student Summary'],
@@ -685,6 +687,8 @@ describe('student.test.ts', { concurrency: false }, () => {
685
687
  const expectedInherits = {
686
688
  create: undefined,
687
689
  select: undefined,
690
+ group: `${controllerBeanFullName}#select`,
691
+ aggregate: `${controllerBeanFullName}#select`,
688
692
  view: undefined,
689
693
  update: undefined,
690
694
  summary: `${controllerBeanFullName}#view`,
@@ -0,0 +1,422 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { ISchemaObjectExtensionField } from 'vona-module-a-openapi';
3
+
4
+ import { ZodMetadata } from '@cabloy/zod-openapi';
5
+ import assert from 'node:assert';
6
+ import { randomUUID } from 'node:crypto';
7
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { describe, it } from 'node:test';
11
+ import { app } from 'vona-mock';
12
+ import { $schema } from 'vona-module-a-openapiutils';
13
+
14
+ import { DtoStudentAggregateRes } from '../src/dto/studentAggregateRes.tsx';
15
+ import { DtoStudentGroupResItem } from '../src/dto/studentGroupResItem.tsx';
16
+ import { DtoStudentSelectReq } from '../src/dto/studentSelectReq.tsx';
17
+
18
+ type StudentGroup = { level: number; count_all: string };
19
+ type StudentAggregate = { count_all: string; count_imageId: string };
20
+
21
+ async function dropStudents(ids: TableIdentity[]) {
22
+ const student = app.scope('training-student');
23
+ for (const id of ids.toReversed()) {
24
+ await student.model.studentContent.delete({ studentId: id }, { disableDeleted: true });
25
+ await student.model.student.deleteById(id, { disableDeleted: true });
26
+ }
27
+ }
28
+
29
+ async function readAnalysis(query: Record<string, unknown>) {
30
+ const groups: StudentGroup[] = await app.bean.executor.performAction(
31
+ 'get',
32
+ '/training/student/group',
33
+ { query },
34
+ );
35
+ const aggregate: StudentAggregate = await app.bean.executor.performAction(
36
+ 'get',
37
+ '/training/student/aggregate',
38
+ { query },
39
+ );
40
+ return {
41
+ groups: groups.sort((a, b) => a.level - b.level),
42
+ aggregate,
43
+ };
44
+ }
45
+
46
+ describe('studentAnalysis.test.ts', { concurrency: false }, () => {
47
+ it('keeps select unchanged and emits optional analysis response field supersets', async () => {
48
+ await app.bean.executor.mockCtx(async () => {
49
+ const request = $schema(DtoStudentSelectReq);
50
+ const requestMetadata = ZodMetadata.getOpenapiMetadata(request) as
51
+ | ISchemaObjectExtensionField
52
+ | undefined;
53
+ assert.equal(requestMetadata?.filter?.table, 'trainingStudent');
54
+ assert.equal(request.safeParse({}).success, true);
55
+ assert.equal(
56
+ request.safeParse({ name: 'Student', level: 2, createdAt: '2026-03-10~2026-03-10' })
57
+ .success,
58
+ true,
59
+ );
60
+ assert.equal(
61
+ request.safeParse({
62
+ where: { level: { _gte_: 2 } },
63
+ columns: ['name'],
64
+ orders: 'level,desc',
65
+ pageNo: 2,
66
+ pageSize: 1,
67
+ }).success,
68
+ true,
69
+ );
70
+ assert.equal(request.safeParse({ pageSize: 0 }).success, false);
71
+ // Filter transforms belong to runtime schema metadata; public OpenAPI strips them.
72
+ const createdAtSchema = ZodMetadata.getFieldSchema(request, 'createdAt');
73
+ assert.ok(createdAtSchema);
74
+ const createdAtMetadata = ZodMetadata.getOpenapiMetadata(createdAtSchema) as
75
+ | ISchemaObjectExtensionField
76
+ | undefined;
77
+ assert.equal(createdAtMetadata?.filter?.transform?.[0], 'a-web:dateRange');
78
+ for (const [DtoClass, expectedFields] of [
79
+ [
80
+ DtoStudentSelectReq,
81
+ ['columns', 'createdAt', 'level', 'name', 'orders', 'pageNo', 'pageSize', 'where'],
82
+ ],
83
+ [DtoStudentGroupResItem, ['count_all', 'count_imageId', 'level']],
84
+ [DtoStudentAggregateRes, ['count_all', 'count_imageId']],
85
+ ] as const) {
86
+ const document = await app.bean.openapi.generateJsonOfClass(DtoClass, 'V31');
87
+ const component = Object.values(document.components!.schemas!).find(
88
+ item => !('$ref' in item) && Object.hasOwn(item.properties ?? {}, expectedFields[0]),
89
+ );
90
+ assert.ok(component && !('$ref' in component));
91
+ assert.deepEqual(Object.keys(component.properties ?? {}).sort(), [...expectedFields]);
92
+ for (const field of expectedFields.filter(field => field.startsWith('count_'))) {
93
+ const schema = component.properties![field];
94
+ assert.ok(schema && !('$ref' in schema));
95
+ // v.optional() emits a nullable string in the V31 schema dialect.
96
+ assert.deepEqual(schema.type, ['string', 'null']);
97
+ }
98
+ }
99
+ assert.deepEqual($schema(DtoStudentGroupResItem).parse({ count_imageId: '1' }), {
100
+ count_imageId: '1',
101
+ });
102
+ assert.deepEqual($schema(DtoStudentGroupResItem).parse({ level: 2, count_all: '3' }), {
103
+ level: 2,
104
+ count_all: '3',
105
+ });
106
+ assert.deepEqual(
107
+ $schema(DtoStudentAggregateRes).parse({ count_all: '3', count_imageId: '1' }),
108
+ { count_all: '3', count_imageId: '1' },
109
+ );
110
+ });
111
+ });
112
+
113
+ it('filters real API results and refreshes warmed counts after create, update and soft delete', async () => {
114
+ await app.bean.executor.mockCtx(async () => {
115
+ const jwt = await app.bean.passport.signinMock();
116
+ const ids: TableIdentity[] = [];
117
+ const name = `Analysis-${randomUUID()}`;
118
+ let imageId: TableIdentity | undefined;
119
+ let directory: string | undefined;
120
+ const student = app.scope('training-student');
121
+ try {
122
+ const empty = {
123
+ groups: [],
124
+ aggregate: { count_all: '0', count_imageId: '0' },
125
+ };
126
+ assert.deepEqual(await readAnalysis({ name }), empty);
127
+ directory = await mkdtemp(path.join(os.tmpdir(), 'student-analysis-'));
128
+ const file = path.join(directory, 'student.png');
129
+ await writeFile(
130
+ file,
131
+ Buffer.from(
132
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIHWP4z8DwHwAFAAH/e+m+7wAAAABJRU5ErkJggg==',
133
+ 'base64',
134
+ ),
135
+ );
136
+ const image = await app.bean.image.upload(
137
+ 'image-native:native',
138
+ { file, filename: 'student.png', contentType: 'image/png' },
139
+ { imageScene: 'training-student:studentImage' },
140
+ );
141
+ imageId = image.id;
142
+ for (const [index, level] of [1, 1, 2].entries()) {
143
+ const id = await app.bean.executor.performAction('post', '/training/student', {
144
+ body: {
145
+ name: `${name}-${index}`,
146
+ mobile: '13812345678',
147
+ level,
148
+ ...(index === 0 ? { imageId } : {}),
149
+ },
150
+ });
151
+ ids.push(id);
152
+ await student.model.student.updateById(id, {
153
+ createdAt: new Date(index === 2 ? '2026-03-11T12:00:00Z' : '2026-03-10T12:00:00Z'),
154
+ });
155
+ }
156
+ const initial = {
157
+ groups: [
158
+ { level: 1, count_all: '2' },
159
+ { level: 2, count_all: '1' },
160
+ ],
161
+ aggregate: { count_all: '3', count_imageId: '1' },
162
+ };
163
+ assert.deepEqual(await readAnalysis({ name }), initial);
164
+ assert.deepEqual(await readAnalysis({ name }), initial);
165
+ assert.deepEqual(
166
+ await app.bean.executor.performAction('get', '/training/student/group', {
167
+ query: { name, aggrs: { count: ['imageId'] } },
168
+ }),
169
+ [
170
+ { level: 1, count_imageId: '1' },
171
+ { level: 2, count_imageId: '0' },
172
+ ],
173
+ );
174
+ assert.deepEqual(
175
+ await student.service.student.group({
176
+ where: { name: { _startsWith_: name } },
177
+ groups: ['level'],
178
+ }),
179
+ initial.groups,
180
+ );
181
+ assert.deepEqual(
182
+ await student.service.student.aggregate({
183
+ where: { name: { _startsWith_: name } },
184
+ aggrs: { count: ['imageId'] },
185
+ }),
186
+ { count_imageId: '1' },
187
+ );
188
+ const selected = await app.bean.executor.performAction('get', '/training/student', {
189
+ query: { name, pageSize: 1 },
190
+ });
191
+ assert.equal(selected.total, '3');
192
+ assert.equal(selected.list.length, 1);
193
+ assert.equal(Object.hasOwn(selected.list[0], 'count_all'), false);
194
+ assert.deepEqual(await readAnalysis({ name, level: 2 }), {
195
+ groups: [{ level: 2, count_all: '1' }],
196
+ aggregate: { count_all: '1', count_imageId: '0' },
197
+ });
198
+ assert.deepEqual(await readAnalysis({ name, createdAt: '2026-03-10~2026-03-10' }), {
199
+ groups: [{ level: 1, count_all: '2' }],
200
+ aggregate: { count_all: '2', count_imageId: '1' },
201
+ });
202
+ assert.deepEqual(await readAnalysis({ name: `${name}-absent` }), empty);
203
+
204
+ // Dispatch concrete URLs through the HTTP router, where /:id could otherwise win.
205
+ for (const [action, expected] of [
206
+ ['group', initial.groups],
207
+ ['aggregate', initial.aggregate],
208
+ ] as const) {
209
+ const url = new URL(app.util.getAbsoluteUrlByApiPath(`/training/student/${action}`));
210
+ url.searchParams.set('name', name);
211
+ const response = await fetch(url, {
212
+ headers: {
213
+ Accept: 'application/json',
214
+ Authorization: `Bearer ${jwt.accessToken}`,
215
+ },
216
+ });
217
+ assert.equal(response.status, 200);
218
+ const body = await response.json();
219
+ assert.equal(body.code, 0, JSON.stringify(body));
220
+ const actual =
221
+ action === 'group'
222
+ ? body.data.sort((a: StudentGroup, b: StudentGroup) => a.level - b.level)
223
+ : body.data;
224
+ assert.deepEqual(actual, expected);
225
+ }
226
+ for (const [action, controls, expected] of [
227
+ [
228
+ 'group',
229
+ { groups: ['level'], aggrs: { count: ['imageId'] } },
230
+ [
231
+ { level: 1, count_imageId: '1' },
232
+ { level: 2, count_imageId: '0' },
233
+ ],
234
+ ],
235
+ ['aggregate', { aggrs: { count: ['*'] } }, { count_all: '3' }],
236
+ ] as const) {
237
+ const url = new URL(app.util.getAbsoluteUrlByApiPath(`/training/student/${action}`));
238
+ url.searchParams.set('name', name);
239
+ for (const [key, value] of Object.entries(controls)) {
240
+ url.searchParams.set(key, JSON.stringify(value));
241
+ }
242
+ const response = await fetch(url, {
243
+ headers: {
244
+ Accept: 'application/json',
245
+ Authorization: `Bearer ${jwt.accessToken}`,
246
+ },
247
+ });
248
+ assert.equal(response.status, 200);
249
+ const body = await response.json();
250
+ assert.equal(body.code, 0, JSON.stringify(body));
251
+ assert.deepEqual(body.data, expected);
252
+ }
253
+ for (const [action, field, value] of [
254
+ ['group', 'groups', 'level'],
255
+ ['group', 'groups', '["level"'],
256
+ ['group', 'aggrs', '{"count":'],
257
+ ['aggregate', 'aggrs', '{"count":'],
258
+ ]) {
259
+ const url = new URL(app.util.getAbsoluteUrlByApiPath(`/training/student/${action}`));
260
+ url.searchParams.set('name', name);
261
+ url.searchParams.set(field, value);
262
+ const response = await fetch(url, {
263
+ headers: {
264
+ Accept: 'application/json',
265
+ Authorization: `Bearer ${jwt.accessToken}`,
266
+ },
267
+ });
268
+ assert.equal(response.status, 422);
269
+ assert.equal((await response.json()).code, 422);
270
+ }
271
+ for (const [action, controls] of [
272
+ ['', { groups: ['level'] }],
273
+ ['', { aggrs: { count: ['*'] } }],
274
+ ['group', { groups: [] }],
275
+ ['group', { groups: ['level', 'level'] }],
276
+ ['group', { groups: ['name'] }],
277
+ ['group', { aggrs: {} }],
278
+ ['group', { aggrs: { count: [] } }],
279
+ ['group', { aggrs: { count: ['*', '*'] } }],
280
+ ['group', { aggrs: { count: ['mobile'] } }],
281
+ ['group', { aggrs: { sum: ['level'] } }],
282
+ ['aggregate', { groups: ['level'] }],
283
+ ['aggregate', { groups: [] }],
284
+ ] as const) {
285
+ await assert.rejects(
286
+ () =>
287
+ app.bean.executor.performAction(
288
+ 'get',
289
+ action === '' ? '/training/student' : `/training/student/${action}`,
290
+ {
291
+ query: { name, ...controls },
292
+ },
293
+ ),
294
+ (error: { code?: number }) => error.code === 422,
295
+ );
296
+ }
297
+ await assert.rejects(
298
+ () => student.service.student.group({ groups: ['name'] } as never),
299
+ (error: { code?: number }) => error.code === 422,
300
+ );
301
+ await assert.rejects(
302
+ () => student.service.student.aggregate({ groups: ['level'] } as never),
303
+ (error: { code?: number }) => error.code === 422,
304
+ );
305
+ assert.deepEqual(
306
+ await readAnalysis({
307
+ name,
308
+ columns: ['name'],
309
+ orders: 'level,desc',
310
+ pageNo: 2,
311
+ pageSize: 1,
312
+ }),
313
+ initial,
314
+ );
315
+ // Raw predicates are shared with select and take precedence over named filters.
316
+ assert.deepEqual(
317
+ await readAnalysis({
318
+ name,
319
+ level: 1,
320
+ where: { level: 2 },
321
+ }),
322
+ {
323
+ groups: [{ level: 2, count_all: '1' }],
324
+ aggregate: { count_all: '1', count_imageId: '0' },
325
+ },
326
+ );
327
+ for (const action of ['group', 'aggregate'] as const) {
328
+ for (const query of [
329
+ { name, pageSize: 0 },
330
+ { name, pageNo: 0 },
331
+ ]) {
332
+ await assert.rejects(
333
+ () =>
334
+ app.bean.executor.performAction('get', `/training/student/${action}`, { query }),
335
+ (error: { code?: number }) => error.code === 422,
336
+ );
337
+ }
338
+ }
339
+ await app.bean.executor.performAction('patch', '/training/student/:id', {
340
+ params: { id: ids[1] },
341
+ body: { name: `${name}-1`, mobile: '13812345678', level: 2 },
342
+ });
343
+ assert.deepEqual(await readAnalysis({ name }), {
344
+ groups: [
345
+ { level: 1, count_all: '1' },
346
+ { level: 2, count_all: '2' },
347
+ ],
348
+ aggregate: { count_all: '3', count_imageId: '1' },
349
+ });
350
+ await app.bean.executor.performAction('delete', '/training/student/:id', {
351
+ params: { id: ids[0] },
352
+ });
353
+ assert.equal(
354
+ (await student.model.student.getById(ids[0], { disableDeleted: true }))?.deleted,
355
+ true,
356
+ );
357
+ assert.deepEqual(await readAnalysis({ name }), {
358
+ groups: [{ level: 2, count_all: '2' }],
359
+ aggregate: { count_all: '2', count_imageId: '0' },
360
+ });
361
+ } finally {
362
+ try {
363
+ await dropStudents(ids);
364
+ if (imageId !== undefined) await app.bean.image.delete(imageId);
365
+ } finally {
366
+ if (directory) await rm(directory, { recursive: true, force: true });
367
+ await app.bean.passport.signout();
368
+ }
369
+ }
370
+ });
371
+ });
372
+
373
+ it('excludes students owned by another instance', async () => {
374
+ // Keep the full UUID within the VARCHAR(50) name column.
375
+ const name = `Analysis-${randomUUID()}`;
376
+ const ids: TableIdentity[] = [];
377
+ try {
378
+ await app.bean.executor.mockCtx(async () => {
379
+ await app.bean.passport.signinMock();
380
+ try {
381
+ ids.push(
382
+ await app.bean.executor.performAction('post', '/training/student', {
383
+ body: { name, mobile: '13812345678', level: 3 },
384
+ }),
385
+ );
386
+ assert.deepEqual(await readAnalysis({ name }), {
387
+ groups: [{ level: 3, count_all: '1' }],
388
+ aggregate: { count_all: '1', count_imageId: '0' },
389
+ });
390
+ } finally {
391
+ await app.bean.passport.signout();
392
+ }
393
+ });
394
+ await app.bean.executor.mockCtx(
395
+ async () => {
396
+ await app.bean.passport.signinMock();
397
+ try {
398
+ assert.deepEqual(await readAnalysis({ name }), {
399
+ groups: [],
400
+ aggregate: { count_all: '0', count_imageId: '0' },
401
+ });
402
+ } finally {
403
+ await app.bean.passport.signout();
404
+ }
405
+ },
406
+ { instanceName: 'shareTest' as any },
407
+ );
408
+ await app.bean.executor.mockCtx(async () => {
409
+ assert.deepEqual(
410
+ await app.scope('training-student').service.student.aggregate({
411
+ where: { name },
412
+ }),
413
+ { count_all: '1', count_imageId: '0' },
414
+ );
415
+ });
416
+ } finally {
417
+ await app.bean.executor.mockCtx(async () => {
418
+ await dropStudents(ids);
419
+ });
420
+ }
421
+ });
422
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.19",
3
+ "version": "5.0.20",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.71",
3
+ "version": "5.1.72",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -10,7 +10,7 @@
10
10
  "vona-module-a-cabloy": "workspace:^",
11
11
  "vona-module-a-layoutprofile": "^5.0.2",
12
12
  "vona-module-a-markdown": "^5.0.3",
13
- "vona-module-a-rbac": "^5.0.19",
13
+ "vona-module-a-rbac": "^5.0.20",
14
14
  "vona-module-a-socket": "workspace:^",
15
15
  "vona-module-a-ssr": "workspace:^",
16
16
  "vona-module-a-ssrhmr": "workspace:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-captcha",
3
- "version": "5.1.16",
3
+ "version": "5.1.17",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-captcha",
3
- "version": "5.1.20",
3
+ "version": "5.1.21",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-core",
3
- "version": "5.1.70",
3
+ "version": "5.1.71",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-hmr",
3
- "version": "5.1.16",
3
+ "version": "5.1.17",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-locale",
3
- "version": "5.1.13",
3
+ "version": "5.1.14",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapi",
3
- "version": "5.1.27",
3
+ "version": "5.1.28",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapiutils",
3
- "version": "5.1.24",
3
+ "version": "5.1.25",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -38,7 +38,7 @@ export function getTargetDecoratorDtoOpenapi(
38
38
  replace: true,
39
39
  });
40
40
  }
41
- return appMetadata.getOwnMetadata(SymbolDecoratorDtoOpenapi, target);
41
+ return appMetadata.getMetadata(SymbolDecoratorDtoOpenapi, target);
42
42
  }
43
43
 
44
44
  export function setTargetDecoratorDtoOpenapi(
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-orm",
3
- "version": "5.1.39",
3
+ "version": "5.1.40",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "db",
6
6
  "keywords": [