vona-cli-set-api 1.1.174 → 1.1.176

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 (26) hide show
  1. package/cli/templates/tools/crudAnalysis/aggregate/src/dto/{{resourceName}}AggregateRes.tsx_ +11 -0
  2. package/cli/templates/tools/crudAnalysis/common/src/lib/{{resourceName}}Analysis.ts_ +14 -0
  3. package/cli/templates/tools/crudAnalysis/common/test/{{resourceName}}Analysis.test.ts_ +81 -0
  4. package/cli/templates/tools/crudAnalysis/group/src/dto/{{resourceName}}GroupResItem.tsx_ +11 -0
  5. package/cli/templates/tools/crudBasic/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  6. package/cli/templates/tools/crudBasic/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  7. package/cli/templates/tools/crudBasic/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  8. package/cli/templates/tools/crudBasic/boilerplate/test/{{resourceName}}.test.ts_ +4 -0
  9. package/cli/templates/tools/crudBasic/snippets/3-en-us.ts +2 -0
  10. package/cli/templates/tools/crudBasic/snippets/4-zh-cn.ts +2 -0
  11. package/cli/templates/tools/crudStart/boilerplate/src/controller/{{resourceName}}.ts_ +18 -1
  12. package/cli/templates/tools/crudStart/boilerplate/src/dto/{{resourceName}}SelectReq.tsx_ +2 -0
  13. package/cli/templates/tools/crudStart/boilerplate/src/service/{{resourceName}}.ts_ +14 -1
  14. package/cli/templates/tools/crudStart/boilerplate/test/{{resourceName}}.test.ts_ +1 -1
  15. package/cli/templates/tools/crudStart/snippets/3-en-us.ts +2 -0
  16. package/cli/templates/tools/crudStart/snippets/4-zh-cn.ts +2 -0
  17. package/dist/index.js +15 -4
  18. package/dist/lib/crudAnalysis.d.ts +2 -0
  19. package/dist-cli/templates/tools/crudBasic/snippets/3-en-us.js +2 -0
  20. package/dist-cli/templates/tools/crudBasic/snippets/4-zh-cn.js +2 -0
  21. package/dist-cli/templates/tools/crudStart/snippets/3-en-us.js +2 -0
  22. package/dist-cli/templates/tools/crudStart/snippets/4-zh-cn.js +2 -0
  23. package/dist-toolsIsolate/test.js +3 -6
  24. package/dist-toolsIsolate/testDiagnostics.d.ts +1 -0
  25. package/dist-toolsIsolate/testDiagnostics.js +6 -0
  26. package/package.json +4 -4
@@ -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 { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
7
+
8
+ export interface IDtoOptions<%=argv.resourceNameCapitalize%>AggregateRes extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptions<%=argv.resourceNameCapitalize%>AggregateRes>()
11
+ export class Dto<%=argv.resourceNameCapitalize%>AggregateRes extends $Dto.analysisAggregate(<%=argv.resourceName%>Analysis) {}
@@ -0,0 +1,14 @@
1
+ import { $defineAnalysis } from 'vona-module-a-orm';
2
+
3
+ import { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.resourceName%>.ts';
4
+
5
+ // One allowlist drives request controls, response fields, and bounded execution.
6
+ export const <%=argv.resourceName%>Analysis = $defineAnalysis(() => Model<%=argv.resourceNameCapitalize%>, {
7
+ groups: ['name'],
8
+ aggrs: { count: ['*'] },
9
+ defaults: {
10
+ group: { groups: ['name'], aggrs: { count: ['*'] } },
11
+ aggregate: { aggrs: { count: ['*'] } },
12
+ },
13
+ limits: { maxGroups: 3, maxMetrics: 10, maxGroupRows: 1000 },
14
+ });
@@ -0,0 +1,81 @@
1
+ import assert from 'node:assert/strict';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { describe, it } from 'node:test';
4
+ import { app } from 'vona-mock';
5
+
6
+ import { <%=argv.resourceName%>Analysis } from '../src/lib/<%=argv.resourceName%>Analysis.ts';
7
+
8
+ describe('<%=argv.resourceName%>Analysis.test.ts', { concurrency: false }, () => {
9
+ it('returns default and requested-only string counts with validated controls', async () => {
10
+ await app.bean.executor.mockCtx(async () => {
11
+ const scope = app.scope('<%=argv.moduleInfo.relativeName%>');
12
+ const name = `__analysis_${randomUUID()}`;
13
+ const fixture = await scope.model.<%=argv.resourceName%>.insert({ name });
14
+ try {
15
+ await app.bean.passport.signinMock();
16
+ const queries = [
17
+ { name },
18
+ // List controls are validated but do not paginate the analysis population.
19
+ { where: { id: fixture.id }, columns: ['id'], orders: 'name,desc', pageNo: 2, pageSize: 1 },
20
+ ];
21
+ for (const query of queries) {
22
+ const groups = await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/group', { query });
23
+ assert.deepEqual(groups, [{ name, count_all: '1' }]);
24
+ assert.deepEqual(await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/group', {
25
+ query: { ...query, groups: JSON.stringify(['name']), aggrs: JSON.stringify({ count: ['*'] }) },
26
+ }), groups);
27
+ assert.deepEqual(await scope.service.<%=argv.resourceName%>.group({
28
+ where: { id: fixture.id }, groups: ['name'], aggrs: { count: ['*'] },
29
+ }), groups);
30
+ const aggregate = await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/aggregate', { query });
31
+ assert.deepEqual(aggregate, { count_all: '1' });
32
+ assert.deepEqual(await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/aggregate', {
33
+ query: { ...query, aggrs: JSON.stringify({ count: ['*'] }) },
34
+ }), aggregate);
35
+ assert.deepEqual(await scope.service.<%=argv.resourceName%>.aggregate({
36
+ where: { id: fixture.id }, aggrs: { count: ['*'] },
37
+ }), aggregate);
38
+ }
39
+ for (const action of ['group', 'aggregate'] as const) {
40
+ for (const controls of [
41
+ { pageNo: 2, pageSize: 0 },
42
+ { aggrs: JSON.stringify({}) },
43
+ { aggrs: JSON.stringify({ count: ['*', '*'] }) },
44
+ { aggrs: JSON.stringify({ count: ['unknown'] }) },
45
+ { aggrs: '{invalid-json' },
46
+ ]) {
47
+ await assert.rejects(() => app.bean.executor.performAction('get', `<%=argv.moduleActionPathRaw%>/${action}`, {
48
+ query: { name, ...controls },
49
+ }));
50
+ }
51
+ }
52
+ // The shared policy does not enable analysis controls on ordinary select.
53
+ await assert.rejects(() => app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>', {
54
+ query: { name, aggrs: JSON.stringify({ count: ['*'] }) },
55
+ }));
56
+ for (const groups of [[], ['name', 'name'], ['description'], ['unknown']]) {
57
+ await assert.rejects(() => app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/group', {
58
+ query: { name, groups: JSON.stringify(groups) },
59
+ }));
60
+ }
61
+ await assert.rejects(() => scope.service.<%=argv.resourceName%>.group({ aggrs: { count: [] } }));
62
+ assert.deepEqual(<%=argv.resourceName%>Analysis.defaults.group, {
63
+ groups: ['name'], aggrs: { count: ['*'] },
64
+ });
65
+ await assert.rejects(() => app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/aggregate', {
66
+ query: { name, groups: JSON.stringify(['name']) },
67
+ }));
68
+ await assert.rejects(() => scope.service.<%=argv.resourceName%>.aggregate({ aggrs: { count: [] } }));
69
+ assert.deepEqual(<%=argv.resourceName%>Analysis.defaults.aggregate, { aggrs: { count: ['*'] } });
70
+ await scope.model.<%=argv.resourceName%>.deleteById(fixture.id);
71
+ for (const query of queries) {
72
+ assert.deepEqual(await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/group', { query }), []);
73
+ assert.deepEqual(await app.bean.executor.performAction('get', '<%=argv.moduleActionPathRaw%>/aggregate', { query }), { count_all: '0' });
74
+ }
75
+ } finally {
76
+ await scope.model.<%=argv.resourceName%>.deleteById(fixture.id, { disableDeleted: true });
77
+ await app.bean.passport.signout();
78
+ }
79
+ });
80
+ });
81
+ });
@@ -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 { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
7
+
8
+ export interface IDtoOptions<%=argv.resourceNameCapitalize%>GroupResItem extends IDecoratorDtoOptions {}
9
+
10
+ @Dto<IDtoOptions<%=argv.resourceNameCapitalize%>GroupResItem>()
11
+ export class Dto<%=argv.resourceNameCapitalize%>GroupResItem extends $Dto.analysisGroup(<%=argv.resourceName%>Analysis) {}
@@ -1,5 +1,5 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams } from 'vona-module-a-orm';
2
+ import type { IQueryParams, TypeAnalysisGroupParams, TypeAnalysisAggregateParams } from 'vona-module-a-orm';
3
3
  import type { IDecoratorControllerOptions } from 'vona-module-a-web';
4
4
 
5
5
  import { BeanBase } from 'vona';
@@ -17,6 +17,9 @@ import { Dto<%=argv.resourceNameCapitalize%>SelectReq } from '../dto/<%=argv.res
17
17
  import { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
18
18
  import { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.resourceName%>Update.tsx';
19
19
  import { Dto<%=argv.resourceNameCapitalize%>View } from '../dto/<%=argv.resourceName%>View.tsx';
20
+ import { Dto<%=argv.resourceNameCapitalize%>GroupResItem } from '../dto/<%=argv.resourceName%>GroupResItem.tsx';
21
+ import { Dto<%=argv.resourceNameCapitalize%>AggregateRes } from '../dto/<%=argv.resourceName%>AggregateRes.tsx';
22
+ import type { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
20
23
 
21
24
  export interface IControllerOptions<%=argv.resourceNameCapitalize%> extends IDecoratorControllerOptions {}
22
25
 
@@ -39,6 +42,20 @@ export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
39
42
  return await this.scope.service.<%=argv.resourceName%>.select(params);
40
43
  }
41
44
 
45
+ @Web.get('group', { summary: $locale('<%=argv.resourceNameCapitalize%>Group') })
46
+ @Api.body(v.array(v.object(Dto<%=argv.resourceNameCapitalize%>GroupResItem)))
47
+ @Passport.rbac({ actionInherit: 'select' })
48
+ async group(@Arg.filter(Dto<%=argv.resourceNameCapitalize%>SelectReq, { mode: 'group' }) params: TypeAnalysisGroupParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>GroupResItem[]> {
49
+ return await this.scope.service.<%=argv.resourceName%>.group(params);
50
+ }
51
+
52
+ @Web.get('aggregate', { summary: $locale('<%=argv.resourceNameCapitalize%>Aggregate') })
53
+ @Api.body(Dto<%=argv.resourceNameCapitalize%>AggregateRes)
54
+ @Passport.rbac({ actionInherit: 'select' })
55
+ async aggregate(@Arg.filter(Dto<%=argv.resourceNameCapitalize%>SelectReq, { mode: 'aggregate' }) params: TypeAnalysisAggregateParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>AggregateRes> {
56
+ return await this.scope.service.<%=argv.resourceName%>.aggregate(params);
57
+ }
58
+
42
59
  @Web.get(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>View') })
43
60
  @Api.body(v.optional(), v.object(Dto<%=argv.resourceNameCapitalize%>View))
44
61
  @Passport.rbac()
@@ -7,10 +7,12 @@ import z from 'zod';
7
7
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
8
8
 
9
9
  import { Entity<%=argv.resourceNameCapitalize%> } from '../entity/<%=argv.resourceName%>.tsx';
10
+ import { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
10
11
 
11
12
  export interface IDtoOptions<%=argv.resourceNameCapitalize%>SelectReq extends IDecoratorDtoOptions {}
12
13
 
13
14
  @Dto<IDtoOptions<%=argv.resourceNameCapitalize%>SelectReq>({
15
+ analysis: <%=argv.resourceName%>Analysis,
14
16
  openapi: { filter: { table: '<%=argv.moduleResourceName%>' } },
15
17
  fields: {
16
18
  name: $makeSchema(v.optional(), z.string()),
@@ -1,9 +1,10 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams } from 'vona-module-a-orm';
2
+ import type { IQueryParams, TypeAnalysisGroupParams, TypeAnalysisAggregateParams } from 'vona-module-a-orm';
3
3
 
4
4
  import { BeanBase } from 'vona';
5
5
  import { Service } from 'vona-module-a-bean';
6
6
  import { Core } from 'vona-module-a-core';
7
+ import { $executeAnalysisGroup, $executeAnalysisAggregate } from 'vona-module-a-orm';
7
8
 
8
9
  import type { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
9
10
  import type { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
@@ -11,6 +12,10 @@ import type { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.r
11
12
  import type { Dto<%=argv.resourceNameCapitalize%>View } from '../dto/<%=argv.resourceName%>View.tsx';
12
13
  import type { Entity<%=argv.resourceNameCapitalize%> } from '../entity/<%=argv.resourceName%>.tsx';
13
14
  import type { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.resourceName%>.ts';
15
+ import type { Dto<%=argv.resourceNameCapitalize%>GroupResItem } from '../dto/<%=argv.resourceName%>GroupResItem.tsx';
16
+ import type { Dto<%=argv.resourceNameCapitalize%>AggregateRes } from '../dto/<%=argv.resourceName%>AggregateRes.tsx';
17
+
18
+ import { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
14
19
 
15
20
  @Service()
16
21
  export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
@@ -22,6 +27,14 @@ export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
22
27
  return await this.scope.model.<%=argv.resourceName%>.selectAndCount(params);
23
28
  }
24
29
 
30
+ async group(params?: TypeAnalysisGroupParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>GroupResItem[]> {
31
+ return await $executeAnalysisGroup(this.scope.model.<%=argv.resourceName%>, <%=argv.resourceName%>Analysis, params);
32
+ }
33
+
34
+ async aggregate(params?: TypeAnalysisAggregateParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>AggregateRes> {
35
+ return await $executeAnalysisAggregate(this.scope.model.<%=argv.resourceName%>, <%=argv.resourceName%>Analysis, params);
36
+ }
37
+
25
38
  async view(id: TableIdentity): Promise<Dto<%=argv.resourceNameCapitalize%>View | undefined> {
26
39
  return await this.scope.model.<%=argv.resourceName%>.getById(id);
27
40
  }
@@ -29,6 +29,8 @@ describe('<%=argv.resourceName%>.test.ts', { concurrency: false }, () => {
29
29
  const expectedSummaries = {
30
30
  create: ['post', moduleApiPath, 'Create <%=argv.resourceNameCapitalize%>'],
31
31
  select: ['get', moduleApiPath, 'Query <%=argv.resourceNameCapitalize%> List'],
32
+ group: ['get', `${moduleApiPath}/group`, 'Group <%=argv.resourceNameCapitalize%>'],
33
+ aggregate: ['get', `${moduleApiPath}/aggregate`, 'Aggregate <%=argv.resourceNameCapitalize%>'],
32
34
  view: ['get', `${moduleApiPath}/{id}`, 'View <%=argv.resourceNameCapitalize%>'],
33
35
  update: ['patch', `${moduleApiPath}/{id}`, 'Update <%=argv.resourceNameCapitalize%>'],
34
36
  delete: ['delete', `${moduleApiPath}/{id}`, 'Delete <%=argv.resourceNameCapitalize%>'],
@@ -54,6 +56,8 @@ describe('<%=argv.resourceName%>.test.ts', { concurrency: false }, () => {
54
56
  const expectedInherits = {
55
57
  create: undefined,
56
58
  select: undefined,
59
+ group: `${controllerBeanFullName}#select`,
60
+ aggregate: `${controllerBeanFullName}#select`,
57
61
  view: undefined,
58
62
  update: undefined,
59
63
  delete: undefined,
@@ -40,6 +40,8 @@ export default metadataCustomSnippet({
40
40
  [`${resource}Update`]: `Update ${resource}`,
41
41
  [`${resource}Delete`]: `Delete ${resource}`,
42
42
  [`${resource}DeleteBulk`]: `Bulk Delete ${resource}`,
43
+ [`${resource}Group`]: `Group ${resource}`,
44
+ [`${resource}Aggregate`]: `Aggregate ${resource}`,
43
45
  };
44
46
  return locale_transform({ ast, argv, resources });
45
47
  },
@@ -23,6 +23,8 @@ export default metadataCustomSnippet({
23
23
  [`${resource}Update`]: `更新${resource}`,
24
24
  [`${resource}Delete`]: `删除${resource}`,
25
25
  [`${resource}DeleteBulk`]: `批量删除${resource}`,
26
+ [`${resource}Group`]: `${resource}分组统计`,
27
+ [`${resource}Aggregate`]: `${resource}汇总统计`,
26
28
  };
27
29
  return locale_transform({ ast, argv, resources });
28
30
  },
@@ -1,5 +1,5 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams } from 'vona-module-a-orm';
2
+ import type { IQueryParams, TypeAnalysisGroupParams, TypeAnalysisAggregateParams } from 'vona-module-a-orm';
3
3
  import type { IRbacScopeAccess } from 'vona-module-a-rbac';
4
4
  import type { IDecoratorControllerOptions } from 'vona-module-a-web';
5
5
 
@@ -18,6 +18,9 @@ import { Dto<%=argv.resourceNameCapitalize%>SelectReq } from '../dto/<%=argv.res
18
18
  import { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
19
19
  import { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.resourceName%>Update.tsx';
20
20
  import { Dto<%=argv.resourceNameCapitalize%>View } from '../dto/<%=argv.resourceName%>View.tsx';
21
+ import { Dto<%=argv.resourceNameCapitalize%>GroupResItem } from '../dto/<%=argv.resourceName%>GroupResItem.tsx';
22
+ import { Dto<%=argv.resourceNameCapitalize%>AggregateRes } from '../dto/<%=argv.resourceName%>AggregateRes.tsx';
23
+ import type { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
21
24
 
22
25
  export interface IControllerOptions<%=argv.resourceNameCapitalize%> extends IDecoratorControllerOptions {}
23
26
 
@@ -40,6 +43,20 @@ export class Controller<%=argv.resourceNameCapitalize%> extends BeanBase {
40
43
  return await this.scope.service.<%=argv.resourceName%>.select(params);
41
44
  }
42
45
 
46
+ @Web.get('group', { summary: $locale('<%=argv.resourceNameCapitalize%>Group') })
47
+ @Api.body(v.array(v.object(Dto<%=argv.resourceNameCapitalize%>GroupResItem)))
48
+ @Passport.rbac({ actionInherit: 'select' })
49
+ async group(@Arg.filter(Dto<%=argv.resourceNameCapitalize%>SelectReq, { mode: 'group' }) params: TypeAnalysisGroupParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>GroupResItem[]> {
50
+ return await this.scope.service.<%=argv.resourceName%>.group(params);
51
+ }
52
+
53
+ @Web.get('aggregate', { summary: $locale('<%=argv.resourceNameCapitalize%>Aggregate') })
54
+ @Api.body(Dto<%=argv.resourceNameCapitalize%>AggregateRes)
55
+ @Passport.rbac({ actionInherit: 'select' })
56
+ async aggregate(@Arg.filter(Dto<%=argv.resourceNameCapitalize%>SelectReq, { mode: 'aggregate' }) params: TypeAnalysisAggregateParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>AggregateRes> {
57
+ return await this.scope.service.<%=argv.resourceName%>.aggregate(params);
58
+ }
59
+
43
60
  @Web.get(':id', { summary: $locale('<%=argv.resourceNameCapitalize%>View') })
44
61
  @Api.body(v.optional(), v.object(Dto<%=argv.resourceNameCapitalize%>View))
45
62
  @Passport.rbac()
@@ -7,10 +7,12 @@ import z from 'zod';
7
7
  import { ZovaRender } from 'zova-rest-cabloy-start-admin';
8
8
 
9
9
  import { Entity<%=argv.resourceNameCapitalize%> } from '../entity/<%=argv.resourceName%>.tsx';
10
+ import { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
10
11
 
11
12
  export interface IDtoOptions<%=argv.resourceNameCapitalize%>SelectReq extends IDecoratorDtoOptions {}
12
13
 
13
14
  @Dto<IDtoOptions<%=argv.resourceNameCapitalize%>SelectReq>({
15
+ analysis: <%=argv.resourceName%>Analysis,
14
16
  openapi: { filter: { table: '<%=argv.moduleResourceName%>' } },
15
17
  fields: {
16
18
  name: $makeSchema(v.optional(), z.string()),
@@ -1,9 +1,10 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
- import type { IQueryParams } from 'vona-module-a-orm';
2
+ import type { IQueryParams, TypeAnalysisGroupParams, TypeAnalysisAggregateParams } from 'vona-module-a-orm';
3
3
 
4
4
  import { BeanBase } from 'vona';
5
5
  import { Service } from 'vona-module-a-bean';
6
6
  import { Core } from 'vona-module-a-core';
7
+ import { $executeAnalysisGroup, $executeAnalysisAggregate } from 'vona-module-a-orm';
7
8
 
8
9
  import type { Dto<%=argv.resourceNameCapitalize%>Create } from '../dto/<%=argv.resourceName%>Create.tsx';
9
10
  import type { Dto<%=argv.resourceNameCapitalize%>SelectRes } from '../dto/<%=argv.resourceName%>SelectRes.tsx';
@@ -11,6 +12,10 @@ import type { Dto<%=argv.resourceNameCapitalize%>Update } from '../dto/<%=argv.r
11
12
  import type { Dto<%=argv.resourceNameCapitalize%>View } from '../dto/<%=argv.resourceName%>View.tsx';
12
13
  import type { Entity<%=argv.resourceNameCapitalize%> } from '../entity/<%=argv.resourceName%>.tsx';
13
14
  import type { Model<%=argv.resourceNameCapitalize%> } from '../model/<%=argv.resourceName%>.ts';
15
+ import type { Dto<%=argv.resourceNameCapitalize%>GroupResItem } from '../dto/<%=argv.resourceName%>GroupResItem.tsx';
16
+ import type { Dto<%=argv.resourceNameCapitalize%>AggregateRes } from '../dto/<%=argv.resourceName%>AggregateRes.tsx';
17
+
18
+ import { <%=argv.resourceName%>Analysis } from '../lib/<%=argv.resourceName%>Analysis.ts';
14
19
 
15
20
  @Service()
16
21
  export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
@@ -22,6 +27,14 @@ export class Service<%=argv.resourceNameCapitalize%> extends BeanBase {
22
27
  return await this.scope.model.<%=argv.resourceName%>.selectAndCount(params);
23
28
  }
24
29
 
30
+ async group(params?: TypeAnalysisGroupParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>GroupResItem[]> {
31
+ return await $executeAnalysisGroup(this.scope.model.<%=argv.resourceName%>, <%=argv.resourceName%>Analysis, params);
32
+ }
33
+
34
+ async aggregate(params?: TypeAnalysisAggregateParams<typeof <%=argv.resourceName%>Analysis>): Promise<Dto<%=argv.resourceNameCapitalize%>AggregateRes> {
35
+ return await $executeAnalysisAggregate(this.scope.model.<%=argv.resourceName%>, <%=argv.resourceName%>Analysis, params);
36
+ }
37
+
25
38
  async view(id: TableIdentity): Promise<Dto<%=argv.resourceNameCapitalize%>View | undefined> {
26
39
  return await this.scope.model.<%=argv.resourceName%>.getById(id);
27
40
  }
@@ -19,7 +19,7 @@ describe('<%=argv.resourceName%>.test.ts', () => {
19
19
  await app.bean.passport.signinMock();
20
20
  try {
21
21
  app.bean.passport.current!.roles = [];
22
- const actions = ['create', 'select', 'view', 'update', 'delete'];
22
+ const actions = ['create', 'select', 'view', 'update', 'delete', 'group', 'aggregate'];
23
23
  const permissions = await Promise.all(
24
24
  actions.map(action =>
25
25
  app.bean.permission.retrievePermissionAction(
@@ -40,6 +40,8 @@ export default metadataCustomSnippet({
40
40
  [`${resource}View`]: `View ${resource}`,
41
41
  [`${resource}Update`]: `Update ${resource}`,
42
42
  [`${resource}Delete`]: `Delete ${resource}`,
43
+ [`${resource}Group`]: `Group ${resource}`,
44
+ [`${resource}Aggregate`]: `Aggregate ${resource}`,
43
45
  };
44
46
  return locale_transform({ ast, argv, resources });
45
47
  },
@@ -23,6 +23,8 @@ export default metadataCustomSnippet({
23
23
  [`${resource}View`]: `查看${resource}`,
24
24
  [`${resource}Update`]: `更新${resource}`,
25
25
  [`${resource}Delete`]: `删除${resource}`,
26
+ [`${resource}Group`]: `${resource}分组统计`,
27
+ [`${resource}Aggregate`]: `${resource}汇总统计`,
26
28
  };
27
29
  return locale_transform({ ast, argv, resources });
28
30
  },
package/dist/index.js CHANGED
@@ -1134,9 +1134,6 @@ class CliBinTest extends BeanCliBase {
1134
1134
  if (argv.coverage) {
1135
1135
  args.push('--experimental-test-coverage');
1136
1136
  }
1137
- if (process.env.TEST_WHYISNODERUNNING === 'true') {
1138
- args.push('--import=why-is-node-running/include');
1139
- }
1140
1137
  args = args.concat([getImportEsm(resolveVonaLocalPaths(projectPath, 'test').generatedDir), testFile, projectPath, (!!argv.coverage).toString(), patterns.join(',')]);
1141
1138
  // args = args.concat(['--experimental-transform-types', getImportEsm(), testFile, projectPath, (!!argv.coverage).toString(), patterns.join(',')]);
1142
1139
  await runTestChild(args, projectPath);
@@ -1968,6 +1965,16 @@ class CliToolsCrud extends BeanCliBase {
1968
1965
  }
1969
1966
  }
1970
1967
 
1968
+ async function renderCrudAnalysis(cli, targetDir, setName) {
1969
+ for (const part of ['common', 'group', 'aggregate']) {
1970
+ await cli.template.renderBoilerplateAndSnippets({
1971
+ targetDir,
1972
+ setName,
1973
+ boilerplatePath: `tools/crudAnalysis/${part}`
1974
+ });
1975
+ }
1976
+ }
1977
+
1971
1978
  class CliToolsCrudBasic extends BeanCliBase {
1972
1979
  async execute() {
1973
1980
  const {
@@ -1999,6 +2006,7 @@ class CliToolsCrudBasic extends BeanCliBase {
1999
2006
  snippetsPath: 'tools/crudBasic/snippets',
2000
2007
  boilerplatePath: 'tools/crudBasic/boilerplate'
2001
2008
  });
2009
+ await renderCrudAnalysis(this, targetDir, __ThisSetName__);
2002
2010
  // tools.metadata
2003
2011
  if (!argv.nometadata) {
2004
2012
  await this.helper.invokeCli([':tools:metadata', moduleName], {
@@ -2039,6 +2047,7 @@ class CliToolsCrudStart extends BeanCliBase {
2039
2047
  snippetsPath: 'tools/crudStart/snippets',
2040
2048
  boilerplatePath: 'tools/crudStart/boilerplate'
2041
2049
  });
2050
+ await renderCrudAnalysis(this, targetDir, __ThisSetName__);
2042
2051
  // tools.metadata
2043
2052
  if (!argv.nometadata) {
2044
2053
  await this.helper.invokeCli([':tools:metadata', moduleName], {
@@ -2662,6 +2671,8 @@ class CliToolsMasterDetail extends BeanCliBase {
2662
2671
  service: path.join(detailRoot, 'src/service', `${argv.detailResourceName}.ts`),
2663
2672
  dtoDir: path.join(detailRoot, 'src/dto'),
2664
2673
  test: path.join(detailRoot, 'test', `${argv.detailResourceName}.test.ts`),
2674
+ analysisTest: path.join(detailRoot, 'test', `${argv.detailResourceName}Analysis.test.ts`),
2675
+ analysis: path.join(detailRoot, 'src/lib', `${argv.detailResourceName}Analysis.ts`),
2665
2676
  metaVersion: path.join(detailRoot, 'src/bean', 'meta.version.ts'),
2666
2677
  metaIndex: path.join(detailRoot, 'src/bean', 'meta.index.ts')
2667
2678
  };
@@ -2672,7 +2683,7 @@ class CliToolsMasterDetail extends BeanCliBase {
2672
2683
  }
2673
2684
  _removeStandaloneDetailSurface() {
2674
2685
  const files = this._detailPaths();
2675
- for (const file of [files.controller, files.service, files.test]) {
2686
+ for (const file of [files.controller, files.service, files.test, files.analysisTest, files.analysis]) {
2676
2687
  if (fs.existsSync(file)) {
2677
2688
  fs.rmSync(file, {
2678
2689
  force: true
@@ -0,0 +1,2 @@
1
+ import type { BeanCliBase } from '@cabloy/cli';
2
+ export declare function renderCrudAnalysis(cli: BeanCliBase, targetDir: string, setName: string): Promise<void>;
@@ -30,6 +30,8 @@ export default metadataCustomSnippet({
30
30
  [`${resource}Update`]: `Update ${resource}`,
31
31
  [`${resource}Delete`]: `Delete ${resource}`,
32
32
  [`${resource}DeleteBulk`]: `Bulk Delete ${resource}`,
33
+ [`${resource}Group`]: `Group ${resource}`,
34
+ [`${resource}Aggregate`]: `Aggregate ${resource}`,
33
35
  };
34
36
  return locale_transform({ ast, argv, resources });
35
37
  },
@@ -20,6 +20,8 @@ export default metadataCustomSnippet({
20
20
  [`${resource}Update`]: `更新${resource}`,
21
21
  [`${resource}Delete`]: `删除${resource}`,
22
22
  [`${resource}DeleteBulk`]: `批量删除${resource}`,
23
+ [`${resource}Group`]: `${resource}分组统计`,
24
+ [`${resource}Aggregate`]: `${resource}汇总统计`,
23
25
  };
24
26
  return locale_transform({ ast, argv, resources });
25
27
  },
@@ -30,6 +30,8 @@ export default metadataCustomSnippet({
30
30
  [`${resource}View`]: `View ${resource}`,
31
31
  [`${resource}Update`]: `Update ${resource}`,
32
32
  [`${resource}Delete`]: `Delete ${resource}`,
33
+ [`${resource}Group`]: `Group ${resource}`,
34
+ [`${resource}Aggregate`]: `Aggregate ${resource}`,
33
35
  };
34
36
  return locale_transform({ ast, argv, resources });
35
37
  },
@@ -20,6 +20,8 @@ export default metadataCustomSnippet({
20
20
  [`${resource}View`]: `查看${resource}`,
21
21
  [`${resource}Update`]: `更新${resource}`,
22
22
  [`${resource}Delete`]: `删除${resource}`,
23
+ [`${resource}Group`]: `${resource}分组统计`,
24
+ [`${resource}Aggregate`]: `${resource}汇总统计`,
23
25
  };
24
26
  return locale_transform({ ast, argv, resources });
25
27
  },
@@ -10,7 +10,7 @@ import { run } from 'node:test';
10
10
  import { lcov, spec } from 'node:test/reporters';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { cast, createGeneralApp } from 'vona-core';
13
- import whyIsNodeRunning from 'why-is-node-running';
13
+ import { formatActiveResourceCounts } from "./testDiagnostics.js";
14
14
  const argv = process.argv.slice(2);
15
15
  const projectPath = argv[0];
16
16
  const coverage = argv[1] === 'true';
@@ -66,13 +66,10 @@ async function testRun(projectPath, coverage, patterns) {
66
66
  const closeApplication = async () => {
67
67
  const [_, error] = await catchError(() => app.close());
68
68
  closeError = error;
69
- // handles
69
+ // resource types only; no stack/source inspection
70
70
  if (process.env.TEST_WHYISNODERUNNING === 'true') {
71
71
  await sleep(2000);
72
- const handles = process._getActiveHandles();
73
- if (handles.length > 3) {
74
- whyIsNodeRunning();
75
- }
72
+ console.error(`Active resource counts: ${formatActiveResourceCounts(process.getActiveResourcesInfo())}`);
76
73
  }
77
74
  };
78
75
  const closeApplicationOnce = () => {
@@ -0,0 +1 @@
1
+ export declare function formatActiveResourceCounts(types: readonly string[]): string;
@@ -0,0 +1,6 @@
1
+ export function formatActiveResourceCounts(types) {
2
+ const counts = new Map();
3
+ for (const type of types)
4
+ counts.set(type, (counts.get(type) ?? 0) + 1);
5
+ return JSON.stringify(Object.fromEntries([...counts].toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))));
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.174",
4
- "gitHead": "b33ff20d9d60632c7e533b578f06a95a1f895e05",
3
+ "version": "1.1.176",
4
+ "gitHead": "d4ca173cd034aee6e73d037f86745c9919218f80",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
7
7
  "framework",
@@ -63,7 +63,7 @@
63
63
  "@rollup/plugin-typescript": "^12.3.0",
64
64
  "@swc-node/register": "^1.12.1",
65
65
  "babel-plugin-transform-typescript-metadata": "^0.3.2",
66
- "babel-plugin-vona-bean-module": "^1.1.10",
66
+ "babel-plugin-vona-bean-module": "^1.1.11",
67
67
  "chalk": "^3.0.0",
68
68
  "cli-table3": "^0.6.5",
69
69
  "compressing": "^1.10.4",
@@ -77,7 +77,7 @@
77
77
  "rollup": "^4.60.1",
78
78
  "urllib": "^4.9.0",
79
79
  "uuid": "^11.1.0",
80
- "vona-core": "^5.1.54",
80
+ "vona-core": "^5.1.56",
81
81
  "why-is-node-running": "^3.2.2",
82
82
  "yargs-parser": "^22.0.0"
83
83
  }