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.
- package/.cabloy-version +1 -1
- package/.github/workflows/vona-tsc.yml +2 -2
- package/CHANGELOG.md +22 -0
- package/oxlint.config.ts +5 -1
- package/package.json +5 -11
- package/repo-agent-governance/managed-assets.json +9 -9
- package/repo-agent-governance/policies/repository.md +2 -0
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +6 -2
- package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/repo-docs/ai/ai-spec-driven-development.md +6 -2
- package/repo-docs/reference/repo-scripts.md +18 -19
- package/scripts/oxlintPlugins.ts +25 -0
- package/scripts/typecheck.test.ts +107 -94
- package/scripts/typecheck.ts +1 -1
- package/vona/oxlint.config.ts +5 -1
- package/vona/package.original.json +5 -6
- package/vona/packages-cli/cabloy-cli/package.json +1 -1
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/beans.ts +0 -2
- package/vona/packages-cli/cli-set-api/src/lib/commands.ts +0 -2
- package/vona/packages-utils/lint/package.json +3 -3
- package/vona/packages-utils/module-glob/package.json +2 -1
- package/vona/packages-utils/module-glob/src/index.ts +2 -26
- package/vona/packages-utils/module-glob/src/suites.ts +31 -0
- package/vona/packages-utils/module-glob/test/suites.test.ts +108 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +254 -182
- package/vona/pnpm-workspace.yaml +0 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/product.test.ts +2 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/sku.test.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuUniqueness.test.ts +9 -10
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressOwnership.test.ts +43 -16
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressPresentation.test.ts +7 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentIndexes.test.ts +2 -1
- package/vona/src/suite/a-commerce/modules/commerce-promotion/test/couponReservation.test.ts +18 -9
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/cartOwnership.test.ts +28 -11
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +33 -21
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutTransaction.test.ts +6 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +33 -25
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +31 -20
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +30 -30
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +24 -26
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipment.test.ts +7 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/shipmentRefundRace.test.ts +15 -5
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockBalance.test.ts +7 -5
- package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +2 -2
- package/vona/src/suite/a-home/modules/home-user/test/accountActivation.test.ts +2 -1
- package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +53 -42
- package/vona/src/suite/a-home/modules/home-user/test/accountPasswordReset.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/accountPasswordSet.test.ts +2 -1
- package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +12 -7
- package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +23 -11
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/test/rbacCatalogGuard.test.ts +5 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/test/ssrMenuVisibility.test.ts +3 -3
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-metrics/test/middlewareSystem.health.test.ts +33 -12
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/permissions.ts +3 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts +6 -2
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_table.ts +4 -4
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/dto/dtoMutate.ts +38 -9
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/relationsGroup.ts +12 -10
- package/vona/src/suite-vendor/a-vona/modules/a-orm/test/dtoMutate.types.test.ts +129 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-security/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-security/test/origin.test.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-web/cli/controller/metadata/generate.ts +72 -28
- package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-web/test/controllerMetadataParser.test.ts +156 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/vona/tsconfig.json +19 -35
- package/zova/oxlint.config.ts +5 -1
- package/zova/package.original.json +6 -6
- package/zova/packages-cli/cli/package.json +3 -3
- package/zova/packages-cli/cli-set-front/package.json +4 -4
- package/zova/packages-utils/zova-vite/package.json +2 -2
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/pnpm-lock.yaml +195 -65
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.tsc.ts +0 -89
- package/vona/packages-cli/cli-set-api/src/lib/command/bin.tsc.ts +0 -24
- /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
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
111
|
-
assert.ok(
|
|
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]
|
|
114
|
-
assert.
|
|
115
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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: [
|
|
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(
|
|
587
|
+
await app.scope('training-student').model.student.getById(createdStudentId),
|
|
576
588
|
undefined,
|
|
577
589
|
);
|
|
578
590
|
} finally {
|
|
@@ -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
|
-
|
|
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', () => {
|
|
@@ -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: ['
|
|
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('
|
|
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: ['
|
|
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.
|
|
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
|
+
"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:^"
|
|
@@ -110,16 +110,21 @@ function fixture() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function withServerEnv(callback: () => Promise<void>) {
|
|
113
|
-
const
|
|
114
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
280
|
-
[
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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;
|
|
34
|
+
[K in keyof IResourceTableActionRecord]?: IOpenapiPermissionAction;
|
|
35
|
+
} & {
|
|
36
|
+
[action: string]: IOpenapiPermissionAction | undefined;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
export interface IOpenapiPermissions {
|
package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_inner.ts
CHANGED
|
@@ -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<
|
|
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
|
package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/bean.model/bean.model_crud_table.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { TableIdentity } from 'table-identity';
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
-
|
|
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<
|
|
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?:
|
|
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
|
-
> =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
?
|
|
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
|
-
};
|
|
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;
|