cabloy 5.1.249 → 5.1.250

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 (115) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/create-cabloy.yml +50 -3
  3. package/CHANGELOG.md +21 -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 +7 -7
  47. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +18 -0
  48. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +2 -0
  49. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +2 -0
  50. package/vona/src/suite/a-training/modules/training-student/src/controller/student.ts +28 -1
  51. package/vona/src/suite/a-training/modules/training-student/src/dto/studentAggregateRes.tsx +11 -0
  52. package/vona/src/suite/a-training/modules/training-student/src/dto/studentGroupResItem.tsx +11 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectReq.tsx +2 -0
  54. package/vona/src/suite/a-training/modules/training-student/src/lib/studentAnalysis.ts +12 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/service/student.ts +22 -1
  56. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +4 -0
  57. package/vona/src/suite/a-training/modules/training-student/test/studentAnalysis.test.ts +422 -0
  58. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  59. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  60. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  61. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  62. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  64. package/vona/src/suite-vendor/a-vona/modules/a-locale/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +1 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/.metadata/index.ts +26 -26
  70. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/aggregateResult.ts +53 -0
  71. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/controls.ts +163 -0
  72. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/execute.ts +49 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/index.ts +3 -0
  74. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/analysis/policy.ts +140 -0
  75. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_cache.ts +4 -2
  76. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +8 -3
  77. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dto.ts +3 -0
  78. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAggregate.ts +4 -1
  79. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoAnalysis.ts +34 -0
  80. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/index.ts +1 -0
  81. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/analysis.ts +137 -0
  82. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/index.ts +1 -0
  83. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsAggr.ts +1 -1
  84. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/aggregateResult.test.ts +81 -0
  85. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.test.ts +208 -0
  86. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/analysis.types.test.ts +112 -0
  87. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  88. package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +1 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  92. package/vona/src/suite-vendor/a-vona/modules/a-web/src/.metadata/index.ts +40 -40
  93. package/vona/src/suite-vendor/a-vona/modules/a-web/src/bean/pipe.filter.ts +44 -3
  94. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/analysisFilter.ts +69 -0
  95. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/arguments.ts +6 -4
  96. package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/index.ts +1 -0
  97. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
  98. package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/filter.ts +7 -2
  99. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  100. package/zova/packages-cli/cli/package.json +2 -2
  101. package/zova/packages-cli/cli-set-front/package.json +2 -2
  102. package/zova/packages-utils/zova-jsx/package.json +2 -2
  103. package/zova/packages-utils/zova-vite/package.json +1 -1
  104. package/zova/packages-zova/zova/package.json +3 -3
  105. package/zova/packages-zova/zova-core/package.json +1 -1
  106. package/zova/pnpm-lock.yaml +11 -11
  107. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +16 -2
  108. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +201 -7
  109. package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +42 -0
  110. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +10 -0
  111. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  112. package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
  113. package/zova/src/suite-vendor/a-zova/modules/a-table/package.json +1 -1
  114. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  115. package/zova/src/suite-vendor/a-zova/package.json +5 -5
@@ -0,0 +1,385 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { describe, it } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+ import ts from 'typescript';
9
+
10
+ import { LocalTemplate } from '../../cabloy-cli/src/lib/local.template.ts';
11
+ import { CliToolsCrud } from '../src/lib/bean/cli.tools.crud.ts';
12
+ import { CliToolsCrudBasic } from '../src/lib/bean/cli.tools.crudBasic.ts';
13
+ import { CliToolsCrudStart } from '../src/lib/bean/cli.tools.crudStart.ts';
14
+ import { CliToolsMasterDetail } from '../src/lib/bean/cli.tools.masterDetail.ts';
15
+ import crud from '../src/lib/command/tools.crud.ts';
16
+ import basic from '../src/lib/command/tools.crudBasic.ts';
17
+ import start from '../src/lib/command/tools.crudStart.ts';
18
+
19
+ const packageRoot = fileURLToPath(new URL('../', import.meta.url));
20
+ const templateRoot = path.join(packageRoot, 'cli/templates');
21
+ const editions = { basic: CliToolsCrudBasic, start: CliToolsCrudStart };
22
+
23
+ // Exercise the authored templates with the production renderer; packaged templates
24
+ // are checked separately because existing dist-cli output can intentionally be stale.
25
+ class SourceTemplate extends LocalTemplate {
26
+ resolveTemplatePath(_set: string, target: string) {
27
+ return path.isAbsolute(target) ? target : path.join(templateRoot, target);
28
+ }
29
+ }
30
+
31
+ async function fixture<T>(fn: (root: string) => Promise<T>) {
32
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'crud-analysis-'));
33
+ try {
34
+ return await fn(root);
35
+ } finally {
36
+ await fs.rm(root, { recursive: true, force: true });
37
+ }
38
+ }
39
+
40
+ async function makeCli(
41
+ root: string,
42
+ Class: typeof CliToolsCrudBasic | typeof CliToolsCrudStart | typeof CliToolsCrud,
43
+ Template: typeof LocalTemplate = SourceTemplate,
44
+ ) {
45
+ const moduleRoot = path.join(root, 'module');
46
+ await fs.mkdir(path.join(moduleRoot, 'src/bean'), { recursive: true });
47
+ await fs.mkdir(path.join(moduleRoot, 'src/config/locale'), { recursive: true });
48
+ await fs.writeFile(
49
+ path.join(moduleRoot, 'package.json'),
50
+ JSON.stringify({ type: 'module', vonaModule: { fileVersion: 0 } }),
51
+ );
52
+ await fs.writeFile(
53
+ path.join(moduleRoot, 'src/bean/meta.index.ts'),
54
+ "import { Meta } from 'vona-module-a-meta';\n@Meta({ indexes: {} }) class MetaIndex {}\n",
55
+ );
56
+ await fs.writeFile(
57
+ path.join(moduleRoot, 'src/bean/meta.version.ts'),
58
+ 'class MetaVersion { async update(_options: IMetaVersionUpdateOptions) {} }\n',
59
+ );
60
+ for (const locale of ['en-us', 'zh-cn']) {
61
+ await fs.writeFile(
62
+ path.join(moduleRoot, `src/config/locale/${locale}.ts`),
63
+ 'export default {};\n',
64
+ );
65
+ }
66
+ const argv = {
67
+ _: [],
68
+ module: 'test-record',
69
+ resourceName: 'record',
70
+ projectPath: root,
71
+ };
72
+ const cli = new Class({ context: { argv } } as never);
73
+ cli.modulesMeta = { modules: { 'test-record': { root: moduleRoot } } } as never;
74
+ cli.__template = new Template(cli);
75
+ cli.console.log = async () => {};
76
+ cli.helper.formatFile = async () => {};
77
+ const invocations: string[][] = [];
78
+ cli.helper.invokeCli = async args => {
79
+ invocations.push(args);
80
+ };
81
+ return { cli, moduleRoot, invocations };
82
+ }
83
+
84
+ async function filesUnder(root: string): Promise<string[]> {
85
+ const files: string[] = [];
86
+ for (const entry of await fs.readdir(root, { withFileTypes: true })) {
87
+ if (entry.isDirectory()) {
88
+ files.push(
89
+ ...(await filesUnder(path.join(root, entry.name))).map(file => `${entry.name}/${file}`),
90
+ );
91
+ } else {
92
+ files.push(entry.name);
93
+ }
94
+ }
95
+ return files.sort();
96
+ }
97
+
98
+ async function read(root: string, file: string) {
99
+ return await fs.readFile(path.join(root, file), 'utf8');
100
+ }
101
+
102
+ describe('default CRUD analysis generation', { concurrency: false }, () => {
103
+ it('removes analysis flags from command options and usage without adding questions', () => {
104
+ for (const definition of [crud, basic, start]) {
105
+ assert.deepEqual(Object.keys(definition.options), ['module']);
106
+ assert.doesNotMatch(definition.info.usage, /--aggregate|--groupBy/);
107
+ assert.deepEqual(Object.keys(definition.groups.default.questions), [
108
+ 'resourceName',
109
+ 'module',
110
+ ]);
111
+ }
112
+ });
113
+
114
+ for (const [edition, Class] of Object.entries(editions)) {
115
+ it(`${edition}: generates both actions with one finite name/count policy by default`, async () =>
116
+ fixture(async root => {
117
+ const { cli, moduleRoot, invocations } = await makeCli(root, Class);
118
+ await cli.execute();
119
+ const files = await filesUnder(moduleRoot);
120
+ for (const file of [
121
+ 'src/lib/recordAnalysis.ts',
122
+ 'test/recordAnalysis.test.ts',
123
+ 'src/dto/recordGroupResItem.tsx',
124
+ 'src/dto/recordAggregateRes.tsx',
125
+ ]) {
126
+ assert.ok(files.includes(file), file);
127
+ }
128
+ assert.equal(
129
+ files.some(file => file.includes('AnalysisReq')),
130
+ false,
131
+ );
132
+ assert.deepEqual(invocations, [[':tools:metadata', 'test-record']]);
133
+ const controller = await read(moduleRoot, 'src/controller/record.ts');
134
+ const service = await read(moduleRoot, 'src/service/record.ts');
135
+ const req = await read(moduleRoot, 'src/dto/recordSelectReq.tsx');
136
+ const policy = await read(moduleRoot, 'src/lib/recordAnalysis.ts');
137
+ assert.doesNotMatch(controller, /AnalysisReq|\$schema|dataScope/);
138
+ assert.ok(
139
+ controller.includes(
140
+ 'async select(@Arg.filter(DtoRecordSelectReq) params: IQueryParams<ModelRecord>)',
141
+ ),
142
+ );
143
+ assert.match(req, /\$Dto.queryPage\(EntityRecord, \['name', 'createdAt'\]\)/);
144
+ assert.match(req, /analysis: recordAnalysis/);
145
+ assert.match(req, /import \{ recordAnalysis \} from '\.\.\/lib\/recordAnalysis\.ts'/);
146
+ assert.match(
147
+ policy,
148
+ /export const recordAnalysis = \$defineAnalysis\(\(\) => ModelRecord, \{/,
149
+ );
150
+ assert.match(policy, /groups: \['name'\]/);
151
+ assert.match(policy, /aggrs: \{ count: \['\*'\] \}/);
152
+ assert.match(policy, /group: \{ groups: \['name'\], aggrs: \{ count: \['\*'\] \} \}/);
153
+ assert.match(policy, /aggregate: \{ aggrs: \{ count: \['\*'\] \} \}/);
154
+ assert.match(policy, /limits: \{ maxGroups: 3, maxMetrics: 10, maxGroupRows: 1000 \}/);
155
+ assert.doesNotMatch(policy, /description|AnalysisAggrs|AnalysisGroup/);
156
+ assert.doesNotMatch(
157
+ service,
158
+ /\.\.\.params|\.group\(|\.aggregate\(|items\.map|Object\.hasOwn|normalizeRecordCount|count_all:/,
159
+ );
160
+ assert.equal(controller.match(/actionInherit: 'select'/g)?.length, 2);
161
+ for (const action of ['group', 'aggregate']) {
162
+ const mode = action === 'group' ? 'Group' : 'Aggregate';
163
+ assert.ok(
164
+ controller.includes(
165
+ `async ${action}(@Arg.filter(DtoRecordSelectReq, { mode: '${action}' }) params: TypeAnalysis${mode}Params<typeof recordAnalysis>)`,
166
+ ),
167
+ `${action} must derive action controls from the same SelectReq policy`,
168
+ );
169
+ assert.ok(
170
+ controller.indexOf(`@Web.get('${action}'`) < controller.indexOf("@Web.get(':id'"),
171
+ );
172
+ assert.ok(
173
+ service.includes(
174
+ `async ${action}(params?: TypeAnalysis${mode}Params<typeof recordAnalysis>)`,
175
+ ),
176
+ );
177
+ assert.ok(
178
+ service.includes(
179
+ `return await $executeAnalysis${mode}(this.scope.model.record, recordAnalysis, params)`,
180
+ ),
181
+ );
182
+ }
183
+ const groupDto = await read(moduleRoot, 'src/dto/recordGroupResItem.tsx');
184
+ assert.match(groupDto, /\$Dto\.analysisGroup\(recordAnalysis\)/);
185
+ assert.doesNotMatch(groupDto, /description|\$Class|\$makeSchema|zod/);
186
+ assert.match(
187
+ await read(moduleRoot, 'src/dto/recordAggregateRes.tsx'),
188
+ /\$Dto\.analysisAggregate\(recordAnalysis\)/,
189
+ );
190
+ for (const locale of ['en-us', 'zh-cn']) {
191
+ const source = await read(moduleRoot, `src/config/locale/${locale}.ts`);
192
+ assert.match(source, /RecordGroup/);
193
+ assert.match(source, /RecordAggregate/);
194
+ }
195
+ const analysisTest = await read(moduleRoot, 'test/recordAnalysis.test.ts');
196
+ assert.doesNotMatch(analysisTest, /normalizeRecordCount|normalizes only/);
197
+ assert.match(
198
+ analysisTest,
199
+ /returns default and requested-only string counts with validated controls/,
200
+ );
201
+ assert.match(analysisTest, /JSON\.stringify\(\{ count: \['\*'\] \}\)/);
202
+ assert.match(
203
+ analysisTest,
204
+ /The shared policy does not enable analysis controls on ordinary select/,
205
+ );
206
+ assert.match(analysisTest, /\{ name \}/);
207
+ assert.match(analysisTest, /where: \{ id: fixture.id \}/);
208
+ assert.match(
209
+ analysisTest,
210
+ /columns: \['id'\], orders: 'name,desc', pageNo: 2, pageSize: 1/,
211
+ );
212
+ assert.match(analysisTest, /assert.rejects\(/);
213
+ assert.match(analysisTest, /for \(const action of \['group', 'aggregate'\] as const\)/);
214
+ assert.match(analysisTest, /\['description'\]/);
215
+ assert.match(analysisTest, /pageNo: 2, pageSize: 0/);
216
+ const actionTest = await read(moduleRoot, 'test/record.test.ts');
217
+ if (edition === 'basic') {
218
+ assert.match(
219
+ actionTest,
220
+ /group: \['get', `\$\{moduleApiPath\}\/group`, 'Group Record'\]/,
221
+ );
222
+ assert.match(
223
+ actionTest,
224
+ /aggregate: \['get', `\$\{moduleApiPath\}\/aggregate`, 'Aggregate Record'\]/,
225
+ );
226
+ assert.match(actionTest, /group: `\$\{controllerBeanFullName\}#select`/);
227
+ assert.match(actionTest, /aggregate: `\$\{controllerBeanFullName\}#select`/);
228
+ } else {
229
+ assert.match(
230
+ actionTest,
231
+ /const actions = \['create', 'select', 'view', 'update', 'delete', 'group', 'aggregate'\]/,
232
+ );
233
+ }
234
+ for (const file of files.filter(file => /\.tsx?$/.test(file))) {
235
+ const source = await read(moduleRoot, file);
236
+ assert.ok(source.trim(), file);
237
+ assert.doesNotMatch(source, /<%/);
238
+ const result = ts.transpileModule(source, {
239
+ fileName: file,
240
+ reportDiagnostics: true,
241
+ compilerOptions: {
242
+ target: ts.ScriptTarget.ESNext,
243
+ module: ts.ModuleKind.ESNext,
244
+ experimentalDecorators: true,
245
+ },
246
+ });
247
+ assert.deepEqual(
248
+ result.diagnostics?.filter(
249
+ diagnostic => diagnostic.category === ts.DiagnosticCategory.Error,
250
+ ),
251
+ [],
252
+ file,
253
+ );
254
+ }
255
+ }));
256
+
257
+ it(`${edition}: aggregate master-detail cleanup removes generated standalone analysis artifacts`, async () =>
258
+ fixture(async root => {
259
+ const { cli, moduleRoot } = await makeCli(root, Class);
260
+ await cli.execute();
261
+ const masterDetail = new CliToolsMasterDetail({
262
+ context: {
263
+ argv: {
264
+ _detailModule: { root: moduleRoot },
265
+ detailResourceName: 'record',
266
+ createdDetailResource: true,
267
+ detailMode: 'aggregate',
268
+ },
269
+ },
270
+ } as never);
271
+ (masterDetail as any)._handleAggregateDetailMode(false);
272
+ const files = await filesUnder(moduleRoot);
273
+ for (const file of [
274
+ 'src/controller/record.ts',
275
+ 'src/service/record.ts',
276
+ 'src/lib/recordAnalysis.ts',
277
+ 'test/record.test.ts',
278
+ 'test/recordAnalysis.test.ts',
279
+ ]) {
280
+ assert.ok(!files.includes(file), file);
281
+ }
282
+ assert.ok(!files.some(file => file.startsWith('src/dto/')));
283
+ assert.ok(files.includes('src/entity/record.tsx'));
284
+ assert.ok(files.includes('src/model/record.ts'));
285
+ }));
286
+ }
287
+
288
+ it(
289
+ 'freshly packaged beans and production templates match default source generation',
290
+ { skip: process.env.CABLOY_TEST_PACKAGED_CLI !== '1' },
291
+ async () => {
292
+ // Run after tsc:publish. Published packages ship raw EJS in cli/ and compiled
293
+ // snippets in dist-cli/; verify both are packaged, not just present locally.
294
+ const [pack] = JSON.parse(
295
+ execFileSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json'], {
296
+ cwd: packageRoot,
297
+ encoding: 'utf8',
298
+ }),
299
+ ) as { files: { path: string }[] }[];
300
+ const packedFiles = new Set(pack.files.map(file => file.path));
301
+ assert.ok(packedFiles.has('dist/index.js'));
302
+ const { beans, commands } = await import('../dist/index.js');
303
+ for (const command of ['crud', 'crudBasic', 'crudStart']) {
304
+ assert.deepEqual(Object.keys(commands.tools[command].options), ['module']);
305
+ assert.doesNotMatch(commands.tools[command].info.usage, /--aggregate|--groupBy/);
306
+ }
307
+ const previousBrand = process.env.CabloyCliBrandName;
308
+ process.env.CabloyCliBrandName = 'vona';
309
+ try {
310
+ for (const [edition, Class] of Object.entries(editions)) {
311
+ await fixture(async root => {
312
+ const source = await makeCli(path.join(root, 'source'), Class);
313
+ const packaged = await makeCli(
314
+ path.join(root, 'packaged'),
315
+ beans[edition === 'basic' ? 'tools.crudBasic' : 'tools.crudStart'],
316
+ LocalTemplate,
317
+ );
318
+ for (const target of [
319
+ `tools/crud${edition === 'basic' ? 'Basic' : 'Start'}/boilerplate`,
320
+ `tools/crud${edition === 'basic' ? 'Basic' : 'Start'}/snippets`,
321
+ 'tools/crudAnalysis/common',
322
+ 'tools/crudAnalysis/group',
323
+ 'tools/crudAnalysis/aggregate',
324
+ ]) {
325
+ const resolved = packaged.cli.template.resolveTemplatePath('api', target);
326
+ const prefix = target.endsWith('/snippets') ? 'dist-cli/templates' : 'cli/templates';
327
+ assert.equal(resolved, path.join(packageRoot, prefix, target));
328
+ const templateFiles = await filesUnder(resolved);
329
+ assert.ok(templateFiles.length, target);
330
+ for (const file of templateFiles) {
331
+ assert.ok(
332
+ packedFiles.has(`${prefix}/${target}/${file}`),
333
+ `missing packaged template: ${target}/${file}`,
334
+ );
335
+ }
336
+ }
337
+ await source.cli.execute();
338
+ await packaged.cli.execute();
339
+ const files = await filesUnder(source.moduleRoot);
340
+ assert.deepEqual(await filesUnder(packaged.moduleRoot), files);
341
+ for (const file of files) {
342
+ assert.equal(
343
+ await read(packaged.moduleRoot, file),
344
+ await read(source.moduleRoot, file),
345
+ `${edition} ${file}`,
346
+ );
347
+ }
348
+ assert.deepEqual(packaged.invocations, source.invocations);
349
+ });
350
+ }
351
+ } finally {
352
+ if (previousBrand === undefined) delete process.env.CabloyCliBrandName;
353
+ else process.env.CabloyCliBrandName = previousBrand;
354
+ }
355
+ },
356
+ );
357
+
358
+ it('wrapper invokes default edition generation and refreshes metadata after all rendering', async () => {
359
+ for (const edition of ['basic', 'start']) {
360
+ await fixture(async root => {
361
+ const { cli, moduleRoot, invocations } = await makeCli(root, CliToolsCrud);
362
+ await fs.writeFile(
363
+ path.join(root, edition === 'start' ? '__CABLOY_START__' : '__CABLOY_BASIC__'),
364
+ '',
365
+ );
366
+ cli.helper.invokeCli = async args => {
367
+ invocations.push(args);
368
+ if (args[0] === ':tools:metadata') {
369
+ assert.match(await read(moduleRoot, 'src/bean/ssrMenu.record.ts'), /record/);
370
+ }
371
+ };
372
+ await cli.execute();
373
+ assert.deepEqual(invocations, [
374
+ [
375
+ edition === 'start' ? ':tools:crudStart' : ':tools:crudBasic',
376
+ 'record',
377
+ '--module=test-record',
378
+ '--nometadata',
379
+ ],
380
+ [':tools:metadata', 'test-record'],
381
+ ]);
382
+ });
383
+ }
384
+ });
385
+ });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babel-plugin-vona-bean-module",
3
- "version": "1.1.10",
3
+ "version": "1.1.11",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "babel-plugin-vona-bean-module",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.124",
3
+ "version": "5.1.125",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.54",
3
+ "version": "5.1.55",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.54",
3
+ "version": "6.1.55",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -851,8 +851,8 @@ importers:
851
851
  specifier: npm:@cabloy/zod@4.3.8
852
852
  version: '@cabloy/zod@4.3.8'
853
853
  zova:
854
- specifier: ^5.1.178
855
- version: 5.1.178(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3))
854
+ specifier: ^5.1.179
855
+ version: 5.1.179(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)(vue@3.5.43(typescript@5.9.3))
856
856
  zova-jsx:
857
857
  specifier: ^1.1.95
858
858
  version: 1.1.95(typescript@5.9.3)
@@ -917,14 +917,14 @@ importers:
917
917
  specifier: ^5.1.36
918
918
  version: 5.1.36
919
919
  zova-module-a-ssr:
920
- specifier: ^5.1.42
921
- version: 5.1.42
920
+ specifier: ^5.1.43
921
+ version: 5.1.43
922
922
  zova-module-a-ssrhmr:
923
923
  specifier: ^5.1.24
924
924
  version: 5.1.24
925
925
  zova-module-a-ssrserver:
926
- specifier: ^5.1.27
927
- version: 5.1.27
926
+ specifier: ^5.1.28
927
+ version: 5.1.28
928
928
  zova-module-a-style:
929
929
  specifier: ^5.1.36
930
930
  version: 5.1.36
@@ -1975,7 +1975,7 @@ importers:
1975
1975
  specifier: ^5.0.3
1976
1976
  version: link:modules/a-markdown
1977
1977
  vona-module-a-rbac:
1978
- specifier: ^5.0.19
1978
+ specifier: ^5.0.20
1979
1979
  version: link:modules/a-rbac
1980
1980
  vona-module-a-socket:
1981
1981
  specifier: workspace:^
@@ -216,8 +216,10 @@ export * from '../dto/detailRecordBase.tsx';
216
216
  export * from '../dto/detailRecordMutate.tsx';
217
217
  export * from '../dto/detailRecordResItem.tsx';
218
218
  export * from '../dto/detailRecordView.tsx';
219
+ export * from '../dto/studentAggregateRes.tsx';
219
220
  export * from '../dto/studentCreate.tsx';
220
221
  export * from '../dto/studentDeleteBulk.tsx';
222
+ export * from '../dto/studentGroupResItem.tsx';
221
223
  export * from '../dto/studentSelectReq.tsx';
222
224
  export * from '../dto/studentSelectRes.tsx';
223
225
  export * from '../dto/studentSelectResItem.tsx';
@@ -228,8 +230,10 @@ import type { IDtoOptionsDetailRecordBase } from '../dto/detailRecordBase.tsx';
228
230
  import type { IDtoOptionsDetailRecordMutate } from '../dto/detailRecordMutate.tsx';
229
231
  import type { IDtoOptionsDetailRecordResItem } from '../dto/detailRecordResItem.tsx';
230
232
  import type { IDtoOptionsDetailRecordView } from '../dto/detailRecordView.tsx';
233
+ import type { IDtoOptionsStudentAggregateRes } from '../dto/studentAggregateRes.tsx';
231
234
  import type { IDtoOptionsStudentCreate } from '../dto/studentCreate.tsx';
232
235
  import type { IDtoOptionsStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
236
+ import type { IDtoOptionsStudentGroupResItem } from '../dto/studentGroupResItem.tsx';
233
237
  import type { IDtoOptionsStudentSelectReq } from '../dto/studentSelectReq.tsx';
234
238
  import type { IDtoOptionsStudentSelectRes } from '../dto/studentSelectRes.tsx';
235
239
  import type { IDtoOptionsStudentSelectResItem } from '../dto/studentSelectResItem.tsx';
@@ -244,8 +248,10 @@ declare module 'vona-module-a-web' {
244
248
  'training-student:detailRecordMutate': IDtoOptionsDetailRecordMutate;
245
249
  'training-student:detailRecordResItem': IDtoOptionsDetailRecordResItem;
246
250
  'training-student:detailRecordView': IDtoOptionsDetailRecordView;
251
+ 'training-student:studentAggregateRes': IDtoOptionsStudentAggregateRes;
247
252
  'training-student:studentCreate': IDtoOptionsStudentCreate;
248
253
  'training-student:studentDeleteBulk': IDtoOptionsStudentDeleteBulk;
254
+ 'training-student:studentGroupResItem': IDtoOptionsStudentGroupResItem;
249
255
  'training-student:studentSelectReq': IDtoOptionsStudentSelectReq;
250
256
  'training-student:studentSelectRes': IDtoOptionsStudentSelectRes;
251
257
  'training-student:studentSelectResItem': IDtoOptionsStudentSelectResItem;
@@ -265,8 +271,10 @@ import type { DtoDetailRecordBase } from '../dto/detailRecordBase.tsx';
265
271
  import type { DtoDetailRecordMutate } from '../dto/detailRecordMutate.tsx';
266
272
  import type { DtoDetailRecordResItem } from '../dto/detailRecordResItem.tsx';
267
273
  import type { DtoDetailRecordView } from '../dto/detailRecordView.tsx';
274
+ import type { DtoStudentAggregateRes } from '../dto/studentAggregateRes.tsx';
268
275
  import type { DtoStudentCreate } from '../dto/studentCreate.tsx';
269
276
  import type { DtoStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
277
+ import type { DtoStudentGroupResItem } from '../dto/studentGroupResItem.tsx';
270
278
  import type { DtoStudentSelectReq } from '../dto/studentSelectReq.tsx';
271
279
  import type { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
272
280
  import type { DtoStudentSelectResItem } from '../dto/studentSelectResItem.tsx';
@@ -291,6 +299,10 @@ declare module 'vona-module-training-student' {
291
299
  fields?: TypeEntityOptionsFields<DtoDetailRecordView, IDtoOptionsDetailRecordView[TypeSymbolKeyFieldsMore]>;
292
300
  }
293
301
 
302
+ export interface IDtoOptionsStudentAggregateRes {
303
+ fields?: TypeEntityOptionsFields<DtoStudentAggregateRes, IDtoOptionsStudentAggregateRes[TypeSymbolKeyFieldsMore]>;
304
+ }
305
+
294
306
  export interface IDtoOptionsStudentCreate {
295
307
  fields?: TypeEntityOptionsFields<DtoStudentCreate, IDtoOptionsStudentCreate[TypeSymbolKeyFieldsMore]>;
296
308
  }
@@ -299,6 +311,10 @@ declare module 'vona-module-training-student' {
299
311
  fields?: TypeEntityOptionsFields<DtoStudentDeleteBulk, IDtoOptionsStudentDeleteBulk[TypeSymbolKeyFieldsMore]>;
300
312
  }
301
313
 
314
+ export interface IDtoOptionsStudentGroupResItem {
315
+ fields?: TypeEntityOptionsFields<DtoStudentGroupResItem, IDtoOptionsStudentGroupResItem[TypeSymbolKeyFieldsMore]>;
316
+ }
317
+
302
318
  export interface IDtoOptionsStudentSelectReq {
303
319
  fields?: TypeEntityOptionsFields<DtoStudentSelectReq, IDtoOptionsStudentSelectReq[TypeSymbolKeyFieldsMore]>;
304
320
  }
@@ -366,6 +382,8 @@ declare module 'vona-module-a-web' {
366
382
  }
367
383
  export interface IApiPathGetRecord{
368
384
  '/training/student': undefined;
385
+ '/training/student/group': undefined;
386
+ '/training/student/aggregate': undefined;
369
387
  '/training/student/:id': undefined;
370
388
  '/training/student/summary/:id': undefined;
371
389
  }
@@ -22,6 +22,8 @@ export default {
22
22
  StudentController: 'Student Management',
23
23
  StudentCreate: 'Create Student',
24
24
  StudentSelect: 'List Students',
25
+ StudentGroup: 'Group Students',
26
+ StudentAggregate: 'Aggregate Students',
25
27
  StudentView: 'View Student',
26
28
  StudentUpdate: 'Update Student',
27
29
  StudentSummary: 'View Student Summary',
@@ -22,6 +22,8 @@ export default {
22
22
  StudentController: '学生管理',
23
23
  StudentCreate: '创建学生',
24
24
  StudentSelect: '查询学生列表',
25
+ StudentGroup: '学生分组统计',
26
+ StudentAggregate: '学生统计',
25
27
  StudentView: '查看学生',
26
28
  StudentUpdate: '更新学生',
27
29
  StudentSummary: '查看学生摘要',
@@ -1,5 +1,9 @@
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
  import type { IDecoratorControllerOptions } from 'vona-module-a-web';
4
8
 
5
9
  import { BeanBase } from 'vona';
@@ -9,11 +13,14 @@ import { Passport } from 'vona-module-a-user';
9
13
  import { Arg, Controller, Web } from 'vona-module-a-web';
10
14
  import { z } from 'zod';
11
15
 
16
+ import type { studentAnalysis } from '../lib/studentAnalysis.ts';
12
17
  import type { ModelStudent } from '../model/student.ts';
13
18
 
14
19
  import { $locale } from '../.metadata/locales.ts';
20
+ import { DtoStudentAggregateRes } from '../dto/studentAggregateRes.tsx';
15
21
  import { DtoStudentCreate } from '../dto/studentCreate.tsx';
16
22
  import { DtoStudentDeleteBulk } from '../dto/studentDeleteBulk.tsx';
23
+ import { DtoStudentGroupResItem } from '../dto/studentGroupResItem.tsx';
17
24
  import { DtoStudentSelectReq } from '../dto/studentSelectReq.tsx';
18
25
  import { DtoStudentSelectRes } from '../dto/studentSelectRes.tsx';
19
26
  import { DtoStudentSummary } from '../dto/studentSummary.tsx';
@@ -44,6 +51,26 @@ export class ControllerStudent extends BeanBase {
44
51
  return await this.scope.service.student.select(params);
45
52
  }
46
53
 
54
+ @Web.get('group', { summary: $locale('StudentGroup') })
55
+ @Api.body(v.array(DtoStudentGroupResItem))
56
+ @Passport.rbac({ actionInherit: 'select' })
57
+ async group(
58
+ @Arg.filter(DtoStudentSelectReq, { mode: 'group' })
59
+ params: TypeAnalysisGroupParams<typeof studentAnalysis>,
60
+ ): Promise<DtoStudentGroupResItem[]> {
61
+ return await this.scope.service.student.group(params);
62
+ }
63
+
64
+ @Web.get('aggregate', { summary: $locale('StudentAggregate') })
65
+ @Api.body(DtoStudentAggregateRes)
66
+ @Passport.rbac({ actionInherit: 'select' })
67
+ async aggregate(
68
+ @Arg.filter(DtoStudentSelectReq, { mode: 'aggregate' })
69
+ params: TypeAnalysisAggregateParams<typeof studentAnalysis>,
70
+ ): Promise<DtoStudentAggregateRes> {
71
+ return await this.scope.service.student.aggregate(params);
72
+ }
73
+
47
74
  @Web.get(':id', { summary: $locale('StudentView') })
48
75
  @Api.body(v.optional(), v.object(DtoStudentView))
49
76
  @Core.serializer()
@@ -0,0 +1,11 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { $Dto } from 'vona-module-a-orm';
4
+ import { Dto } from 'vona-module-a-web';
5
+
6
+ import { studentAnalysis } from '../lib/studentAnalysis.ts';
7
+
8
+ export interface IDtoOptionsStudentAggregateRes extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptionsStudentAggregateRes>()
11
+ export class DtoStudentAggregateRes extends $Dto.analysisAggregate(studentAnalysis) {}
@@ -0,0 +1,11 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { $Dto } from 'vona-module-a-orm';
4
+ import { Dto } from 'vona-module-a-web';
5
+
6
+ import { studentAnalysis } from '../lib/studentAnalysis.ts';
7
+
8
+ export interface IDtoOptionsStudentGroupResItem extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptionsStudentGroupResItem>()
11
+ export class DtoStudentGroupResItem extends $Dto.analysisGroup(studentAnalysis) {}
@@ -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
+ });