cabloy 5.1.225 → 5.1.227

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 (89) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/vona-tsc.yml +2 -2
  3. package/CHANGELOG.md +22 -0
  4. package/oxlint.config.ts +5 -1
  5. package/package.json +5 -11
  6. package/repo-agent-governance/managed-assets.json +9 -9
  7. package/repo-agent-governance/policies/repository.md +2 -0
  8. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +6 -2
  9. package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
  10. package/repo-docs/ai/ai-spec-driven-development.md +6 -2
  11. package/repo-docs/reference/repo-scripts.md +18 -19
  12. package/scripts/oxlintPlugins.ts +25 -0
  13. package/scripts/typecheck.test.ts +107 -94
  14. package/scripts/typecheck.ts +1 -1
  15. package/vona/oxlint.config.ts +5 -1
  16. package/vona/package.original.json +5 -6
  17. package/vona/packages-cli/cabloy-cli/package.json +1 -1
  18. package/vona/packages-cli/cli/package.json +1 -1
  19. package/vona/packages-cli/cli-set-api/package.json +1 -1
  20. package/vona/packages-cli/cli-set-api/src/lib/beans.ts +0 -2
  21. package/vona/packages-cli/cli-set-api/src/lib/commands.ts +0 -2
  22. package/vona/packages-utils/lint/package.json +3 -3
  23. package/vona/packages-utils/module-glob/package.json +2 -1
  24. package/vona/packages-utils/module-glob/src/index.ts +2 -26
  25. package/vona/packages-utils/module-glob/src/suites.ts +31 -0
  26. package/vona/packages-utils/module-glob/test/suites.test.ts +108 -0
  27. package/vona/packages-vona/vona/package.json +1 -1
  28. package/vona/pnpm-lock.yaml +254 -182
  29. package/vona/pnpm-workspace.yaml +0 -1
  30. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/product.test.ts +2 -0
  31. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -0
  32. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuUniqueness.test.ts +9 -10
  33. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +43 -16
  34. package/vona/src/suite/a-commerce/modules/commerce-member/test/addressPresentation.test.ts +7 -1
  35. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +2 -1
  36. package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +18 -9
  37. package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +28 -11
  38. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +33 -21
  39. package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +6 -5
  40. package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +33 -25
  41. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +31 -20
  42. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +30 -30
  43. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +24 -26
  44. package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipment.test.ts +7 -2
  45. package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipmentRefundRace.test.ts +15 -5
  46. package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +7 -5
  47. package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +2 -2
  48. package/vona/src/suite/a-home/modules/home-user/test/accountActivation.test.ts +2 -1
  49. package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +53 -42
  50. package/vona/src/suite/a-home/modules/home-user/test/accountPasswordReset.test.ts +1 -0
  51. package/vona/src/suite/a-home/modules/home-user/test/accountPasswordSet.test.ts +2 -1
  52. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +12 -7
  53. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +23 -11
  54. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  55. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/rbacCatalogGuard.test.ts +5 -1
  56. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  57. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuVisibility.test.ts +3 -3
  58. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  59. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
  61. package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +33 -12
  62. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  63. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/permissions.ts +3 -1
  64. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +6 -2
  66. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_table.ts +4 -4
  67. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/dto/dtoMutate.ts +38 -9
  68. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsGroup.ts +12 -10
  69. package/vona/src/suite-vendor/a-vona/modules/a-orm/test/dtoMutate.types.test.ts +129 -0
  70. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  71. package/vona/src/suite-vendor/a-vona/modules/a-security/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-security/test/origin.test.ts +1 -0
  73. package/vona/src/suite-vendor/a-vona/modules/a-web/cli/controller/metadata/generate.ts +72 -28
  74. package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
  75. package/vona/src/suite-vendor/a-vona/modules/a-web/test/controllerMetadataParser.test.ts +156 -0
  76. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  77. package/vona/tsconfig.json +19 -35
  78. package/zova/oxlint.config.ts +5 -1
  79. package/zova/package.original.json +6 -6
  80. package/zova/packages-cli/cli/package.json +3 -3
  81. package/zova/packages-cli/cli-set-front/package.json +4 -4
  82. package/zova/packages-utils/zova-vite/package.json +2 -2
  83. package/zova/packages-zova/zova/package.json +2 -2
  84. package/zova/pnpm-lock.yaml +195 -65
  85. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  86. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  87. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.tsc.ts +0 -89
  88. package/vona/packages-cli/cli-set-api/src/lib/command/bin.tsc.ts +0 -24
  89. /package/{vona/packages-cli/cli-set-api/src/lib → scripts}/typescriptRunner.ts +0 -0
@@ -15,7 +15,7 @@ interface IAccountPasswordSetFixture {
15
15
  email?: string;
16
16
  }
17
17
 
18
- describe('accountPasswordSet.test.ts', { concurrency: false, sequential: true }, () => {
18
+ describe('accountPasswordSet.test.ts', { concurrency: false }, () => {
19
19
  const releases: Array<() => void> = [];
20
20
 
21
21
  before(async () => {
@@ -587,6 +587,7 @@ async function configurePasswordSet(
587
587
  return await app.bean.executor.mockCtx(async () => {
588
588
  const options =
589
589
  app.bean.onion.middlewareSystem.getOnionSlice('a-security:cors').beanOptions.options;
590
+ assert.ok(options);
590
591
  const whiteListPrevious = options.whiteList;
591
592
  options.whiteList = Array.isArray(whiteList) ? [...whiteList] : whiteList;
592
593
  return () => {
@@ -1,14 +1,13 @@
1
1
  import type { DtoRecordCreate, DtoRecordUpdate } from 'vona-module-training-record';
2
2
  import type { DtoStudentCreate } from 'vona-module-training-student';
3
3
 
4
- import { ZodMetadata } from '@cabloy/zod-openapi';
5
4
  import fse from 'fs-extra';
6
5
  import assert from 'node:assert';
7
6
  import os from 'node:os';
8
7
  import path from 'node:path';
9
8
  import { describe, it } from 'node:test';
10
9
  import { app } from 'vona-mock';
11
- import { getTargetDecoratorRules } from 'vona-module-a-openapiutils';
10
+ import { $makeMetadata, getTargetDecoratorRules } from 'vona-module-a-openapiutils';
12
11
 
13
12
  import { DtoDetailRecordSubjectResItem } from '../src/dto/detailRecordSubjectResItem.tsx';
14
13
  import { DtoRecordSelectReq } from '../src/dto/recordSelectReq.tsx';
@@ -31,7 +30,8 @@ describe('record.test.ts', () => {
31
30
  assert.equal(subjectComponent.required?.includes('_lineNumber'), false);
32
31
 
33
32
  const selectReqRules = getTargetDecoratorRules(DtoRecordSelectReq.prototype);
34
- const studentIdMetadata = ZodMetadata.getOpenapiMetadata(selectReqRules.studentId!);
33
+ assert.ok(selectReqRules.studentId);
34
+ const studentIdMetadata = $makeMetadata(selectReqRules.studentId);
35
35
  assert.equal(selectReqRules.studentId?.type, 'optional');
36
36
  assert.deepEqual(studentIdMetadata?.filter, {
37
37
  table: 'trainingStudent',
@@ -52,13 +52,18 @@ describe('record.test.ts', () => {
52
52
  'V31',
53
53
  );
54
54
  const parameters = controllerApiJson.paths?.['/api/training/record']?.get?.parameters ?? [];
55
- const studentIdParameter = parameters.find(parameter => parameter.name === 'studentId');
55
+ const studentIdParameter = parameters.find(
56
+ parameter => !('$ref' in parameter) && parameter.name === 'studentId',
57
+ );
56
58
  assert.ok(studentIdParameter);
59
+ assert.ok(!('$ref' in studentIdParameter));
57
60
  assert.equal(studentIdParameter.in, 'query');
58
61
  assert.equal(studentIdParameter.required, false);
59
- assert.deepEqual(studentIdParameter.schema?.type, ['string', 'null']);
62
+ assert.ok(studentIdParameter.schema);
63
+ assert.ok(!('$ref' in studentIdParameter.schema));
64
+ assert.deepEqual(studentIdParameter.schema.type, ['string', 'null']);
60
65
  assert.equal(
61
- parameters.some(parameter => parameter.name === 'studentName'),
66
+ parameters.some(parameter => !('$ref' in parameter) && parameter.name === 'studentName'),
62
67
  false,
63
68
  );
64
69
 
@@ -93,7 +98,7 @@ describe('record.test.ts', () => {
93
98
  await app.bean.executor.mockCtx(async () => {
94
99
  const studentData: DtoStudentCreate = {
95
100
  name: '__Student__',
96
- description: 'This is a student for record test',
101
+ content: { descriptionMarkdown: 'This is a student for record test' },
97
102
  mobile: '13812345678',
98
103
  level: 1,
99
104
  trainingRecords: [],
@@ -106,13 +106,20 @@ describe('student.test.ts', { concurrency: false }, () => {
106
106
  );
107
107
  const operation = apiJson.paths?.['/api/training/student/bulk/delete']?.post;
108
108
  assert.equal(operation?.operationId, 'TrainingStudent_deleteBulk');
109
- const requestSchema = operation?.requestBody?.content?.['application/json']?.schema as any;
110
- const requestSchemaRef = requestSchema?.$ref;
111
- assert.ok(requestSchemaRef);
109
+ const requestBody = operation?.requestBody;
110
+ assert.ok(requestBody);
111
+ assert.ok(!('$ref' in requestBody));
112
+ const requestSchema = requestBody.content['application/json']?.schema;
113
+ assert.ok(requestSchema && '$ref' in requestSchema);
114
+ const requestSchemaRef = requestSchema.$ref;
115
+ assert.ok(typeof requestSchemaRef === 'string');
112
116
  const requestSchemaName = requestSchemaRef.replace('#/components/schemas/', '');
113
- const component = apiJson.components?.schemas?.[requestSchemaName] as any;
114
- assert.equal(component?.properties?.ids?.minItems, 1);
115
- assert.equal(component?.properties?.ids?.maxItems, 100);
117
+ const component = apiJson.components?.schemas?.[requestSchemaName];
118
+ assert.ok(component && !('$ref' in component));
119
+ const idsSchema = component.properties?.ids;
120
+ assert.ok(idsSchema && !('$ref' in idsSchema));
121
+ assert.equal(idsSchema.minItems, 1);
122
+ assert.equal(idsSchema.maxItems, 100);
116
123
  });
117
124
  });
118
125
 
@@ -562,17 +569,22 @@ describe('student.test.ts', { concurrency: false }, () => {
562
569
  await app.bean.passport.signinMock();
563
570
  let studentId: string | undefined;
564
571
  try {
565
- studentId = await app.bean.executor.performAction('post', '/training/student', {
566
- body: { name: '__DeleteBulkIdempotent__', mobile: '13891234567', level: 1 },
567
- });
572
+ const createdStudentId = await app.bean.executor.performAction(
573
+ 'post',
574
+ '/training/student',
575
+ {
576
+ body: { name: '__DeleteBulkIdempotent__', mobile: '13891234567', level: 1 },
577
+ },
578
+ );
579
+ studentId = createdStudentId;
568
580
  const deleted = await app.bean.executor.performAction(
569
581
  'post',
570
582
  '/training/student/bulk/delete',
571
- { body: { ids: [studentId, studentId, '-1'] } },
583
+ { body: { ids: [createdStudentId, createdStudentId, '-1'] } },
572
584
  );
573
585
  assert.equal(deleted, null);
574
586
  assert.equal(
575
- await app.scope('training-student').model.student.getById(studentId),
587
+ await app.scope('training-student').model.student.getById(createdStudentId),
576
588
  undefined,
577
589
  );
578
590
  } finally {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.13",
3
+ "version": "5.0.15",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -182,7 +182,11 @@ describe('rbacCatalogGuard.test.ts', { concurrency: false }, () => {
182
182
 
183
183
  it('rejects class-level RBAC decoration', () => {
184
184
  class Controller {}
185
- assert.throws(() => Passport.rbac()(Controller), /must decorate an action/);
185
+ // Deliberately misuse the method decorator as a class decorator to test runtime rejection.
186
+ assert.throws(
187
+ () => (Passport.rbac() as unknown as ClassDecorator)(Controller),
188
+ /must decorate an action/,
189
+ );
186
190
  });
187
191
 
188
192
  it('resolves aliases to the terminal same-controller grant identity', () => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-ssr",
3
- "version": "5.1.34",
3
+ "version": "5.1.35",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -12,7 +12,7 @@ function createMenus() {
12
12
  return [
13
13
  { name: 'test:public', title: 'Public', group: 'root' },
14
14
  { name: 'test:dynamic', title: 'Dynamic', roles: [], group: 'root' },
15
- { name: 'test:static', title: 'Static', roles: ['member'] },
15
+ { name: 'test:static', title: 'Static', roles: ['registeredUser'] },
16
16
  ] as any;
17
17
  }
18
18
 
@@ -20,7 +20,7 @@ describe('ssrMenuVisibility.test.ts', () => {
20
20
  it('distinguishes public, dynamic-only, and static leaves', () => {
21
21
  const menus = createMenus();
22
22
  assert.deepEqual(
23
- resolveSsrMenuVisibilityDefault(menus, roles => roles.includes('member')),
23
+ resolveSsrMenuVisibilityDefault(menus, roles => roles.includes('registeredUser')),
24
24
  [menus[0], menus[2]],
25
25
  );
26
26
  assert.deepEqual(
@@ -44,7 +44,7 @@ describe('ssrMenuVisibility.test.ts', () => {
44
44
  {
45
45
  name: 'test:private',
46
46
  title: 'Private',
47
- roles: ['member'],
47
+ roles: ['registeredUser'],
48
48
  policyRevision: 'private',
49
49
  associationState: 'private',
50
50
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.64",
3
+ "version": "5.1.66",
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.13",
13
+ "vona-module-a-rbac": "^5.0.15",
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-core",
3
- "version": "5.1.65",
3
+ "version": "5.1.66",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-metrics",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -110,16 +110,21 @@ function fixture() {
110
110
  }
111
111
 
112
112
  function withServerEnv(callback: () => Promise<void>) {
113
- const names = ['META_MODE', 'VONA_TEST_RUN_ID', 'VONA_TEST_READY_KEY'] as const;
114
- const old = names.map(name => process.env[name]);
113
+ const old = {
114
+ META_MODE: process.env.META_MODE,
115
+ VONA_TEST_RUN_ID: process.env.VONA_TEST_RUN_ID,
116
+ VONA_TEST_READY_KEY: process.env.VONA_TEST_READY_KEY,
117
+ };
115
118
  process.env.META_MODE = 'test';
116
119
  process.env.VONA_TEST_RUN_ID = runId;
117
120
  process.env.VONA_TEST_READY_KEY = key;
118
121
  return callback().finally(() => {
119
- for (const [index, name] of names.entries()) {
120
- if (old[index] === undefined) delete process.env[name];
121
- else process.env[name] = old[index];
122
- }
122
+ if (old.META_MODE === undefined) Reflect.deleteProperty(process.env, 'META_MODE');
123
+ else process.env.META_MODE = old.META_MODE;
124
+ if (old.VONA_TEST_RUN_ID === undefined) delete process.env.VONA_TEST_RUN_ID;
125
+ else process.env.VONA_TEST_RUN_ID = old.VONA_TEST_RUN_ID;
126
+ if (old.VONA_TEST_READY_KEY === undefined) delete process.env.VONA_TEST_READY_KEY;
127
+ else process.env.VONA_TEST_READY_KEY = old.VONA_TEST_READY_KEY;
123
128
  });
124
129
  }
125
130
 
@@ -276,15 +281,31 @@ describe('authenticated managed shutdown', { concurrency: false }, () => {
276
281
  });
277
282
  }
278
283
 
279
- for (const [name, value] of [
280
- ['META_MODE', 'dev'],
281
- ['VONA_TEST_RUN_ID', '20261014010203-d4e5f6a7'],
282
- ['VONA_TEST_READY_KEY', 'not-a-key'],
283
- ] as const) {
284
+ const invalidServerEnv: [string, () => void][] = [
285
+ [
286
+ 'META_MODE',
287
+ () => {
288
+ process.env.META_MODE = 'dev';
289
+ },
290
+ ],
291
+ [
292
+ 'VONA_TEST_RUN_ID',
293
+ () => {
294
+ process.env.VONA_TEST_RUN_ID = '20261014010203-d4e5f6a7';
295
+ },
296
+ ],
297
+ [
298
+ 'VONA_TEST_READY_KEY',
299
+ () => {
300
+ process.env.VONA_TEST_READY_KEY = 'not-a-key';
301
+ },
302
+ ],
303
+ ];
304
+ for (const [name, change] of invalidServerEnv) {
284
305
  test(`shutdown requires matching server-only ${name}`, async () => {
285
306
  await withServerEnv(async () => {
286
307
  const f = fixture();
287
- process.env[name] = value;
308
+ change();
288
309
  await f.execute();
289
310
  assert.equal(f.ctx.status, 404);
290
311
  assert.equal(f.ctx.res.listenerCount('finish'), 0);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapi",
3
- "version": "5.1.24",
3
+ "version": "5.1.25",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -31,7 +31,9 @@ export interface IOpenapiPermissionActionRbac {
31
31
  export type IOpenapiPermissionAction = boolean | IOpenapiPermissionActionRbac;
32
32
 
33
33
  export type IOpenapiPermissionModeActionActions = {
34
- [K in keyof IResourceTableActionRecord]?: IOpenapiPermissionAction; // IResourceTableActionRecord[K];
34
+ [K in keyof IResourceTableActionRecord]?: IOpenapiPermissionAction;
35
+ } & {
36
+ [action: string]: IOpenapiPermissionAction | undefined;
35
37
  };
36
38
 
37
39
  export interface IOpenapiPermissions {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-orm",
3
- "version": "5.1.36",
3
+ "version": "5.1.38",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "db",
6
6
  "keywords": [
@@ -5,6 +5,8 @@ import { isNil } from '@cabloy/utils';
5
5
  import { cast } from 'vona';
6
6
 
7
7
  import type {
8
+ IBuildModelCountParams,
9
+ IBuildModelSelectParams,
8
10
  IModelCountParams,
9
11
  IModelGetOptionsGeneral,
10
12
  IModelIncrementParams,
@@ -63,7 +65,9 @@ export class BeanModelCrudInner<TRecord extends {}> extends BeanModelView<TRecor
63
65
  return result;
64
66
  }
65
67
 
66
- protected async _select<T extends IModelSelectParams<TRecord>>(
68
+ protected async _select<
69
+ T extends IModelSelectParams<TRecord> | IBuildModelSelectParams<TRecord, undefined, never>,
70
+ >(
67
71
  table?: keyof ITableRecord,
68
72
  params?: T,
69
73
  options?: IModelMethodOptionsGeneral,
@@ -141,7 +145,7 @@ export class BeanModelCrudInner<TRecord extends {}> extends BeanModelView<TRecor
141
145
 
142
146
  protected async _count(
143
147
  table?: keyof ITableRecord,
144
- params?: IModelCountParams<TRecord>,
148
+ params?: IModelCountParams<TRecord> | IBuildModelCountParams<TRecord, undefined, never>,
145
149
  options?: IModelMethodOptionsGeneral,
146
150
  ): Promise<string | undefined> {
147
151
  // table
@@ -1,10 +1,10 @@
1
1
  import type { TableIdentity } from 'table-identity';
2
2
 
3
3
  import type {
4
- IModelCountParams,
4
+ IBuildModelCountParams,
5
+ IBuildModelSelectParams,
5
6
  IModelGetOptionsGeneral,
6
7
  IModelMethodOptionsGeneral,
7
- IModelSelectParams,
8
8
  IModelUpdateOptionsGeneral,
9
9
  ITableRecord,
10
10
  TypeModelWhere,
@@ -23,7 +23,7 @@ export class BeanModelCrudTable<TRecord extends {}> extends BeanModelCrudInner<T
23
23
 
24
24
  async select(
25
25
  table: keyof ITableRecord,
26
- params?: Omit<IModelSelectParams<TRecord>, 'include' | 'with'>,
26
+ params?: Omit<IBuildModelSelectParams<TRecord, undefined, never>, 'include' | 'with'>,
27
27
  options?: IModelMethodOptionsGeneral,
28
28
  ): Promise<TRecord[]> {
29
29
  return await this._select(table, params, options);
@@ -39,7 +39,7 @@ export class BeanModelCrudTable<TRecord extends {}> extends BeanModelCrudInner<T
39
39
 
40
40
  async count(
41
41
  table: keyof ITableRecord,
42
- params?: IModelCountParams<TRecord>,
42
+ params?: IBuildModelCountParams<TRecord, undefined, never>,
43
43
  options?: IModelMethodOptionsGeneral,
44
44
  ): Promise<string | undefined> {
45
45
  return await this._count(table, params, options);
@@ -65,14 +65,16 @@ export type TypeDtoMutateRelationResult<
65
65
  TTopLevel extends boolean | undefined = undefined,
66
66
  TColumns = undefined,
67
67
  REFKEY = undefined,
68
- > = TypeDtoMutateRelationResultEntity<
69
- TRecord,
70
- TColumns extends string | string[] ? TColumns : TypeUtilGetParamsColumns<TOptionsRelation>,
71
- TMutateTypeTopLevel,
72
- TColumnsOmitDefault,
73
- TTopLevel,
74
- REFKEY
75
- > &
68
+ > = (TOptionsRelation extends { dtoClass: Constructable<infer TDto> }
69
+ ? TypeDtoMutateRelationResultDto<TDto, TMutateTypeTopLevel, TTopLevel, REFKEY>
70
+ : TypeDtoMutateRelationResultEntity<
71
+ TRecord,
72
+ TColumns extends string | string[] ? TColumns : TypeUtilGetParamsColumns<TOptionsRelation>,
73
+ TMutateTypeTopLevel,
74
+ TColumnsOmitDefault,
75
+ TTopLevel,
76
+ REFKEY
77
+ >) &
76
78
  (TModel extends BeanModelMeta
77
79
  ? OmitNever<
78
80
  TypeDtoMutateRelationResultMergeInclude<
@@ -158,6 +160,17 @@ type TypeDtoMutateRelationResultMergeWithRelation<
158
160
  >
159
161
  : never;
160
162
 
163
+ // Relation options supply the default dtoClass; an include wrapper may override it.
164
+ type TypeUtilGetDtoMutateRelationParams<Relation, IncludeWrapper> = IncludeWrapper extends {
165
+ dtoClass: Constructable<unknown>;
166
+ }
167
+ ? IncludeWrapper
168
+ : TypeUtilGetRelationOptions<Relation> extends {
169
+ dtoClass: infer TDtoClass extends Constructable<unknown>;
170
+ }
171
+ ? (IncludeWrapper extends {} ? IncludeWrapper : {}) & { dtoClass: TDtoClass }
172
+ : IncludeWrapper;
173
+
161
174
  type TypeUtilGetDtoMutateRelationEntityByType<
162
175
  TMutateTypeTopLevel extends TypeDtoMutateType,
163
176
  Relation,
@@ -167,7 +180,7 @@ type TypeUtilGetDtoMutateRelationEntityByType<
167
180
  TypeUtilGetRelationEntity<Relation>,
168
181
  TypeUtilGetRelationType<Relation>,
169
182
  TypeUtilGetRelationModel<Relation>,
170
- IncludeWrapper,
183
+ TypeUtilGetDtoMutateRelationParams<Relation, IncludeWrapper>,
171
184
  TypeUtilGetColumnsFromRelationAndIncludeWrapper<Relation, IncludeWrapper>,
172
185
  TypeUtilGetRelationKey<Relation>
173
186
  >;
@@ -211,6 +224,22 @@ type TypeUtilGetDtoMutateEntityByType<
211
224
  >
212
225
  | undefined;
213
226
 
227
+ // dtoClass supplies its own fields, so neither columns nor default entity omissions apply.
228
+ // Nested mutations still remove the parent key and patch identity/deletion fields that exist.
229
+ type TypeDtoMutateRelationResultDto<
230
+ TDto,
231
+ TMutateTypeTopLevel extends TypeDtoMutateType,
232
+ TTopLevel extends boolean | undefined,
233
+ REFKEY,
234
+ > = TTopLevel extends false
235
+ ? Omit<
236
+ TMutateTypeTopLevel extends 'create'
237
+ ? Omit<TDto, 'id' | 'deleted'>
238
+ : Partial<Pick<TDto, Extract<keyof TDto, 'id' | 'deleted'>>> & Omit<TDto, 'id' | 'deleted'>,
239
+ Extract<keyof TDto, REFKEY>
240
+ >
241
+ : TDto;
242
+
214
243
  type TypeDtoMutateRelationResultEntity<
215
244
  TRecord,
216
245
  Columns = undefined,
@@ -5,6 +5,8 @@ import type {
5
5
  TypeUtilGetParamsColumns,
6
6
  TypeUtilGetParamsGroups,
7
7
  TypeUtilGetRelationOptionsGroups,
8
+ TypeUtilEntitySelector,
9
+ TypeUtilPrepareColumns,
8
10
  } from './relations.ts';
9
11
  import type {
10
12
  TypeModelAggrRelationResultAggr,
@@ -21,9 +23,9 @@ export type TypeModelGroupRelationResult<TRecord, TOptions> = TypeModelGroupRela
21
23
  TypeUtilGetParamsColumns<TOptions>
22
24
  >;
23
25
 
24
- export type TypeModelGroupRelationResultGroups<TRecord, Aggrs, Groups, Columns> = Groups extends
25
- | string
26
- | string[]
26
+ export type TypeModelGroupRelationResultGroups<TRecord, Aggrs, Groups, Columns> = [Groups] extends [
27
+ string | string[],
28
+ ]
27
29
  ? TypeModelGroupRelationResultGroupsObject<TRecord, Groups, Columns> &
28
30
  (Aggrs extends {}
29
31
  ? TypeRecordAggrsValuesToObject<
@@ -36,11 +38,11 @@ export type TypeModelGroupRelationResultGroups<TRecord, Aggrs, Groups, Columns>
36
38
  : {})
37
39
  : {}; // not use undefined
38
40
 
39
- export type TypeModelGroupRelationResultGroupsObject<TRecord, Groups, Columns> = Columns extends
40
- | string
41
- | string[]
42
- ? { [K in TypeUtilAggrPrepareColumns<Columns>]: K extends keyof TRecord ? TRecord[K] : never }
43
- : Groups extends string | string[]
41
+ export type TypeModelGroupRelationResultGroupsObject<TRecord, Groups, Columns> = [Columns] extends [
42
+ string | string[],
43
+ ]
44
+ ? TypeUtilEntitySelector<TRecord, TypeUtilPrepareColumns<Columns>>
45
+ : [Groups] extends [string | string[]]
44
46
  ? { [K in TypeUtilAggrPrepareColumns<Groups>]: K extends keyof TRecord ? TRecord[K] : never }
45
47
  : {};
46
48
 
@@ -68,8 +70,8 @@ export type TypeModelSelectGroupParamsColumns<
68
70
  TypeModelAggrRelationResultAggrsToNumberType<TypeModelAggrRelationResultAggrs<Aggrs>>;
69
71
 
70
72
  export type TypeModelAggrRelationResultAggrsToNumberType<Columns> = {
71
- [K in keyof Columns]: string;
72
- }; // not use BigNumber or number | string
73
+ [K in keyof Columns]: number | string;
74
+ };
73
75
 
74
76
  export type TypeUtilGetGroupsFromRelationAndIncludeWrapper<
75
77
  Relation,
@@ -0,0 +1,129 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import type { BeanModelMeta } from '../src/lib/bean.model/bean.model_meta.ts';
4
+ import type { TypeDtoMutateResult } from '../src/types/dto/dtoMutate.ts';
5
+ import type { TypeSymbolKeyModelOptions } from '../src/types/relations.ts';
6
+
7
+ // Normalize intersections inside relation arrays before comparing complete shapes.
8
+ type Normalize<T> = T extends Date ? T : T extends object ? { [K in keyof T]: Normalize<T[K]> } : T;
9
+ type Equal<A, B> =
10
+ (<T>() => T extends Normalize<A> ? 1 : 2) extends <T>() => T extends Normalize<B> ? 1 : 2
11
+ ? true
12
+ : false;
13
+ function expectType<_T extends true>() {}
14
+
15
+ type Entity = {
16
+ id: TableIdentity;
17
+ iid: number;
18
+ deleted: boolean;
19
+ createdAt: Date;
20
+ updatedAt: Date;
21
+ parentId: TableIdentity;
22
+ entityOnly: string;
23
+ };
24
+ declare class CustomDto {
25
+ id: TableIdentity;
26
+ parentId: TableIdentity;
27
+ createdAt: Date;
28
+ custom: string[];
29
+ optional?: number;
30
+ }
31
+ type Model<Relations = {}> = BeanModelMeta<Entity> & {
32
+ [K in TypeSymbolKeyModelOptions]: { relations: Relations };
33
+ };
34
+ type ChildModel = Model;
35
+ type ParentModel = Model<{
36
+ children: {
37
+ type: 'hasMany';
38
+ model: new () => ChildModel;
39
+ key: 'parentId';
40
+ options: { columns: ['entityOnly']; dtoClass: typeof CustomDto };
41
+ };
42
+ }>;
43
+
44
+ // Compile-time regressions, included in the normal Vona typecheck without loading the app.
45
+ function checkDtoMutationTypes() {
46
+ type Params = { dtoClass: typeof CustomDto; columns: ['entityOnly'] };
47
+ expectType<
48
+ Equal<TypeDtoMutateResult<ChildModel, Params, 'create', 'createdAt', true>, CustomDto>
49
+ >();
50
+ expectType<Equal<TypeDtoMutateResult<ChildModel, Params, 'update', 'id', true>, CustomDto>>();
51
+ expectType<
52
+ Equal<TypeDtoMutateResult<ChildModel, Params, 'mutate', undefined, true>, CustomDto>
53
+ >();
54
+
55
+ type IncludedCreate = TypeDtoMutateResult<
56
+ ParentModel,
57
+ { include: { children: true } },
58
+ 'create',
59
+ undefined,
60
+ true
61
+ >;
62
+ expectType<
63
+ Equal<IncludedCreate['children'], Array<Omit<CustomDto, 'id' | 'parentId'>> | undefined>
64
+ >();
65
+
66
+ type IncludedUpdate = TypeDtoMutateResult<
67
+ ParentModel,
68
+ { include: { children: { columns: ['entityOnly'] } } },
69
+ 'update',
70
+ undefined,
71
+ true
72
+ >;
73
+ // The existing id becomes optional; absent deleted is not invented, and parentId is removed.
74
+ expectType<
75
+ Equal<
76
+ IncludedUpdate['children'],
77
+ | Array<{
78
+ id?: TableIdentity;
79
+ createdAt: Date;
80
+ custom: string[];
81
+ optional?: number;
82
+ }>
83
+ | undefined
84
+ >
85
+ >();
86
+
87
+ // dtoClass supplied by an include wrapper also overrides the entity without relation defaults.
88
+ type IncludedOverride = TypeDtoMutateResult<
89
+ Model<{ children: { type: 'hasOne'; model: new () => ChildModel; key: 'parentId' } }>,
90
+ { include: { children: Params } },
91
+ 'mutate',
92
+ undefined,
93
+ true
94
+ >;
95
+ expectType<
96
+ Equal<IncludedOverride['children'], NonNullable<IncludedUpdate['children']>[number] | undefined>
97
+ >();
98
+
99
+ // The no-dtoClass path retains its default omissions and relation/column inference.
100
+ expectType<
101
+ Equal<
102
+ TypeDtoMutateResult<ChildModel, undefined, 'create', undefined, true>,
103
+ Pick<Entity, 'parentId' | 'entityOnly'>
104
+ >
105
+ >();
106
+ type DefaultNested = TypeDtoMutateResult<
107
+ ChildModel,
108
+ {
109
+ with: {
110
+ children: {
111
+ type: 'hasMany';
112
+ model: new () => ChildModel;
113
+ key: 'parentId';
114
+ options: { columns: ['entityOnly'] };
115
+ };
116
+ };
117
+ },
118
+ 'update',
119
+ undefined,
120
+ true
121
+ >;
122
+ expectType<
123
+ Equal<
124
+ DefaultNested['children'],
125
+ Array<{ entityOnly: string; id?: TableIdentity; deleted?: boolean }> | undefined
126
+ >
127
+ >();
128
+ }
129
+ void checkDtoMutationTypes;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-permission",
3
- "version": "5.1.29",
3
+ "version": "5.1.31",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-security",
3
- "version": "5.1.15",
3
+ "version": "5.1.16",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [