cabloy 5.1.98 → 5.1.99

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 (139) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/hooks/contract-loop-gate.ts +151 -44
  3. package/CHANGELOG.md +21 -0
  4. package/CLAUDE.md +2 -0
  5. package/cabloy-docs/.vitepress/config.mjs +4 -0
  6. package/cabloy-docs/backend/controller-aop-guide.md +0 -1
  7. package/cabloy-docs/frontend/a-openapi-under-the-hood.md +1 -1
  8. package/cabloy-docs/frontend/generated-contract-consumption-entry-branch.md +1 -1
  9. package/cabloy-docs/frontend/generated-contract-consumption-list-branch.md +1 -1
  10. package/cabloy-docs/frontend/generated-contract-consumption-verify-playbook.md +1 -1
  11. package/cabloy-docs/frontend/model-resource-owner-pattern.md +1 -1
  12. package/cabloy-docs/frontend/model-state-guide.md +3 -1
  13. package/cabloy-docs/frontend/resource-entry-page-deep-dive.md +2 -2
  14. package/cabloy-docs/frontend/resource-list-page-deep-dive.md +1 -1
  15. package/cabloy-docs/frontend/rest-resource-source-reading-map.md +2 -2
  16. package/cabloy-docs/frontend/rest-resource-under-the-hood.md +4 -4
  17. package/cabloy-docs/frontend/use-state-data-best-practices.md +384 -0
  18. package/package.json +1 -1
  19. package/scripts/upgrade.ts +2 -0
  20. package/vona/packages-cli/cli/package.json +1 -1
  21. package/vona/packages-cli/cli-set-api/package.json +1 -1
  22. package/vona/packages-vona/vona/package.json +1 -1
  23. package/vona/packages-vona/vona-core/package.json +1 -1
  24. package/vona/packages-vona/vona-core/src/lib/bean/beanBase.ts +9 -9
  25. package/vona/packages-vona/vona-mock/package.json +1 -1
  26. package/vona/pnpm-lock.yaml +4 -4
  27. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +22 -0
  28. package/vona/src/suite/a-training/modules/training-record/src/bean/meta.version.ts +1 -0
  29. package/vona/src/suite/a-training/modules/training-record/src/config/locale/en-us.ts +1 -0
  30. package/vona/src/suite/a-training/modules/training-record/src/config/locale/zh-cn.ts +1 -0
  31. package/vona/src/suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts +47 -0
  32. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +13 -0
  33. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +13 -0
  34. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +16 -5
  35. package/vona/src/suite/a-training/modules/training-record/src/index.ts +2 -0
  36. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +1 -0
  37. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +34 -0
  38. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +209 -123
  39. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  40. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  41. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +18 -1
  42. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -2
  43. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +1 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +5 -3
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +28 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +8 -0
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyRequest.ts +15 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +30 -0
  49. package/vona/src/suite-vendor/a-file/package.json +2 -2
  50. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  51. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +27 -0
  52. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +8 -0
  53. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyRequest.ts +15 -0
  54. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +27 -0
  55. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +1 -1
  56. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +1 -1
  57. package/vona/src/suite-vendor/a-image/package.json +1 -1
  58. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  59. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  60. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +24 -9
  61. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +87 -73
  62. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionActionByRoles.ts +27 -0
  63. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionUser.ts +20 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  65. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +4 -3
  66. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  67. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +5 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-user/src/.metadata/index.ts +0 -13
  70. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +0 -7
  71. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  72. package/vona/src/suite-vendor/a-vona/modules/a-version/src/bean/meta.version.ts +2 -24
  73. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  74. package/zova/packages-utils/zova-jsx/package.json +2 -2
  75. package/zova/packages-zova/zova/package.json +3 -3
  76. package/zova/packages-zova/zova-core/package.json +1 -1
  77. package/zova/packages-zova/zova-core/src/bean/beanBase.ts +9 -13
  78. package/zova/pnpm-lock.yaml +29 -14
  79. package/zova/src/suite/a-demo/modules/demo-basic/src/page/toolOne/controller.tsx +2 -2
  80. package/zova/src/suite/a-demo/modules/demo-todo/src/page/todo/controller.tsx +2 -2
  81. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +2 -2
  82. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +2 -2
  83. package/zova/src/suite/a-home/modules/home-login/src/page/login/controller.tsx +2 -2
  84. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockDetails/controller.tsx +5 -1
  85. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockTable/controller.tsx +20 -15
  86. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +6 -1
  87. package/zova/src/suite/cabloy-basic/modules/basic-file/cli/openapi.config.ts +9 -0
  88. package/zova/src/suite/cabloy-basic/modules/basic-file/package.json +58 -0
  89. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/component/formFieldFile.ts +31 -0
  90. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +222 -0
  91. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/locales.ts +7 -0
  92. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/this.ts +2 -0
  93. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +121 -0
  94. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/baseURL.ts +5 -0
  95. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/index.ts +3 -0
  96. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +306 -0
  97. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +5718 -0
  98. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +40 -0
  99. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +187 -0
  100. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +534 -0
  101. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/en-us.ts +15 -0
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/zh-cn.ts +15 -0
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/index.ts +4 -0
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +74 -0
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/index.ts +1 -0
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/model/file.ts +23 -0
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/file.ts +12 -0
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/index.ts +1 -0
  109. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.build.json +13 -0
  110. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.json +5 -0
  111. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +61 -0
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +38 -1
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +31 -0
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +52 -0
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +614 -1
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +10 -0
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +13 -0
  118. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +126 -53
  119. package/zova/src/suite/cabloy-basic/modules/basic-image/src/model/image.ts +23 -0
  120. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -4
  121. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +17 -12
  122. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +2 -2
  123. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +2 -2
  124. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/controller.tsx +7 -2
  125. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +34 -33
  126. package/zova/src/suite/cabloy-basic/package.json +1 -0
  127. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  128. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +5 -3
  129. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +2 -2
  130. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +2 -2
  131. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  132. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  133. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +17 -17
  134. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  135. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +8 -3
  136. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  137. package/zova/src/suite-vendor/a-zova/package.json +4 -4
  138. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permission.ts +0 -17
  139. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.userName.ts +0 -35
@@ -89,11 +89,8 @@ export class EntityRecord extends EntityBase {
89
89
  ZovaRender.order(7),
90
90
  ZovaRender.field('basic-image:formFieldImage', {
91
91
  imageScene: 'training-record:sceneImage',
92
- multiple: true,
93
- maxCount: 9,
92
+ maxCount: 3,
94
93
  relationName: 'sceneImages',
95
- accept: ['image/png', 'image/jpeg', 'image/webp'],
96
- maxSize: 2 * 1024 * 1024,
97
94
  enableCrop: false,
98
95
  resize: {
99
96
  width: 1280,
@@ -108,6 +105,20 @@ export class EntityRecord extends EntityBase {
108
105
  )
109
106
  sceneImageIds?: TableIdentity[];
110
107
 
111
- @Api.field(v.title($locale('Description')), v.optional(), ZovaRender.order(8))
108
+ @Api.field(
109
+ v.title($locale('DossierFiles')),
110
+ v.optional(),
111
+ ZovaRender.order(8),
112
+ ZovaRender.field('basic-file:formFieldFile', {
113
+ fileScene: 'training-record:dossierFile',
114
+ relationName: 'dossierFiles',
115
+ maxCount: 3,
116
+ }),
117
+ ZovaRender.cell('basic-file:file', { relationName: 'dossierFiles' }),
118
+ v.array(v.tableIdentity()),
119
+ )
120
+ dossierFileIds?: TableIdentity[];
121
+
122
+ @Api.field(v.title($locale('Description')), v.optional(), ZovaRender.order(9))
112
123
  description?: string;
113
124
  }
@@ -1,4 +1,6 @@
1
+ export * from './bean/fileScene.dossierFile.ts';
1
2
  export * from './bean/imageScene.sceneImage.ts';
3
+ export * from './dto/recordDossierFileView.ts';
2
4
  export * from './lib/index.ts';
3
5
  export * from './.metadata/locales.ts';
4
6
  export * from './.metadata/index.ts';
@@ -1,2 +1,3 @@
1
1
  export * from './onEffectForAverageScore.tsx';
2
2
  export * from './onEffectForTrainingRecordSubjects.tsx';
3
+ export * from './resolveDossierFiles.ts';
@@ -0,0 +1,34 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { VonaContext } from 'vona';
3
+
4
+ import type { DtoRecordDossierFileView } from '../dto/recordDossierFileView.ts';
5
+
6
+ export async function resolveDossierFiles(
7
+ this: VonaContext,
8
+ _value: unknown,
9
+ data: { dossierFileIds?: TableIdentity[] },
10
+ ): Promise<DtoRecordDossierFileView[]> {
11
+ const dossierFileIds = data.dossierFileIds;
12
+ if (!dossierFileIds?.length) return [];
13
+ const items: Array<DtoRecordDossierFileView | undefined> = await Promise.all(
14
+ dossierFileIds.map(async dossierFileId => {
15
+ const file = await this.bean.file.get(dossierFileId);
16
+ if (!file || file.fileScene !== 'training-record:dossierFile') return undefined;
17
+ return {
18
+ id: file.id,
19
+ provider: String(file.provider),
20
+ clientName: file.clientName,
21
+ fileScene: file.fileScene ? String(file.fileScene) : undefined,
22
+ filename: file.filename,
23
+ contentType: file.contentType,
24
+ size: file.size,
25
+ public: file.public,
26
+ uploadedAt: file.uploadedAt,
27
+ meta: file.meta as Record<string, unknown> | undefined,
28
+ downloadUrl: await this.bean.file.getDownloadUrl(file.id),
29
+ signed: !file.public,
30
+ } satisfies DtoRecordDossierFileView;
31
+ }),
32
+ );
33
+ return items.filter((item): item is DtoRecordDossierFileView => !!item);
34
+ }
@@ -1,14 +1,16 @@
1
- import type {
2
- DtoRecordCreate,
3
- DtoRecordSelectRes,
4
- DtoRecordUpdate,
5
- } from 'vona-module-training-record';
1
+ import type { DtoRecordCreate, DtoRecordUpdate } from 'vona-module-training-record';
6
2
  import type { DtoStudentCreate } from 'vona-module-training-student';
7
3
 
4
+ import fse from 'fs-extra';
8
5
  import assert from 'node:assert';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
9
8
  import { describe, it } from 'node:test';
10
9
  import { app } from 'vona-mock';
11
10
 
11
+ const dossierTextAttendance = Buffer.from('attendance dossier file');
12
+ const dossierTextAssessment = Buffer.from('assessment dossier file');
13
+
12
14
  describe('record.test.ts', () => {
13
15
  it('action:record', async () => {
14
16
  await app.bean.executor.mockCtx(async () => {
@@ -21,142 +23,226 @@ describe('record.test.ts', () => {
21
23
  };
22
24
  const trainingTime = new Date('2026-01-15T09:30:00.000Z');
23
25
  const trainingTimeUpdate = new Date('2026-02-20T14:45:00.000Z');
24
- const recordData = {
25
- studentId: 0 as any,
26
- name: '__Record__',
27
- subjectCount: 1,
28
- totalScore: 88,
29
- averageScore: 88,
30
- trainingTime,
31
- description: 'This is a record',
32
- trainingRecordSubjects: [
26
+ const dossierFilePathAttendance = path.join(
27
+ os.tmpdir(),
28
+ 'training-record-dossier-attendance.txt',
29
+ );
30
+ const dossierFilePathAssessment = path.join(
31
+ os.tmpdir(),
32
+ 'training-record-dossier-assessment.txt',
33
+ );
34
+ let dossierFileAttendance: any;
35
+ let dossierFileAssessment: any;
36
+ await app.bean.passport.signinMock();
37
+ try {
38
+ await fse.writeFile(dossierFilePathAttendance, dossierTextAttendance);
39
+ dossierFileAttendance = await app.bean.file.upload(
40
+ 'file-native:native',
33
41
  {
34
- name: '__Math__',
35
- score: 95,
36
- description: 'Math subject',
42
+ file: dossierFilePathAttendance,
43
+ filename: 'attendance.txt',
44
+ contentType: 'text/plain',
45
+ public: false,
46
+ meta: {
47
+ category: 'attendanceSheet',
48
+ source: 'upload',
49
+ },
37
50
  },
38
- ],
39
- } as any as DtoRecordCreate;
40
- await app.bean.passport.signinMock();
41
- const studentId = await app.bean.executor.performAction('post', '/training/student', {
42
- body: studentData,
43
- });
44
- recordData.studentId = studentId;
45
- const recordId = await app.bean.executor.performAction('post', '/training/record', {
46
- body: recordData,
47
- });
48
- assert.equal(!!recordId, true);
51
+ {
52
+ clientName: 'default',
53
+ fileScene: 'training-record:dossierFile',
54
+ },
55
+ );
56
+ const recordData = {
57
+ studentId: 0 as any,
58
+ name: '__Record__',
59
+ subjectCount: 1,
60
+ totalScore: 88,
61
+ averageScore: 88,
62
+ trainingTime,
63
+ dossierFileIds: [dossierFileAttendance.id],
64
+ description: 'This is a record',
65
+ trainingRecordSubjects: [
66
+ {
67
+ name: '__Math__',
68
+ score: 95,
69
+ description: 'Math subject',
70
+ },
71
+ ],
72
+ } as any as DtoRecordCreate;
73
+ const studentId = await app.bean.executor.performAction('post', '/training/student', {
74
+ body: studentData,
75
+ });
76
+ recordData.studentId = studentId;
77
+ const recordId = await app.bean.executor.performAction('post', '/training/record', {
78
+ body: recordData,
79
+ });
80
+ assert.equal(!!recordId, true);
49
81
 
50
- const selectRes: DtoRecordSelectRes = await app.bean.executor.performAction(
51
- 'get',
52
- '/training/record',
53
- );
54
- const recordItem = selectRes.list.find(item => String(item.id) === String(recordId));
55
- assert.equal(!!recordItem, true);
56
- assert.equal(recordItem!.name, recordData.name);
57
- assert.equal(String(recordItem!.studentId), String(studentId));
82
+ const selectRes: any = await app.bean.executor.performAction('get', '/training/record');
83
+ const recordItem = selectRes.list.find((item: any) => String(item.id) === String(recordId));
84
+ assert.equal(!!recordItem, true);
85
+ assert.equal(recordItem!.name, recordData.name);
86
+ assert.equal(String(recordItem!.studentId), String(studentId));
87
+ assert.equal(recordItem!.dossierFiles?.length, 1);
88
+ assert.equal(recordItem!.dossierFiles?.[0]?.filename, 'attendance.txt');
58
89
 
59
- let record: any = await app.bean.executor.performAction('get', '/training/record/:id', {
60
- params: { id: recordId },
61
- });
62
- const recordSubject = record.trainingRecordSubjects?.[0];
63
- assert.equal(record.name, recordData.name);
64
- assert.equal(String(record.studentId), String(studentId));
65
- assert.equal(record.subjectCount, recordData.subjectCount);
66
- assert.equal(record.totalScore, recordData.totalScore);
67
- assert.equal(record.averageScore, recordData.averageScore);
68
- assert.equal(new Date(record.trainingTime).toISOString(), trainingTime.toISOString());
69
- assert.equal(record.trainingRecordSubjects?.length, 1);
70
- assert.equal(recordSubject?.name, '__Math__');
71
- assert.equal(recordSubject?.score, 95);
90
+ let record: any = await app.bean.executor.performAction('get', '/training/record/:id', {
91
+ params: { id: recordId },
92
+ });
93
+ const recordSubject = record.trainingRecordSubjects?.[0];
94
+ const recordDossierFile = record.dossierFiles?.[0];
95
+ assert.equal(record.name, recordData.name);
96
+ assert.equal(String(record.studentId), String(studentId));
97
+ assert.equal(record.subjectCount, recordData.subjectCount);
98
+ assert.equal(record.totalScore, recordData.totalScore);
99
+ assert.equal(record.averageScore, recordData.averageScore);
100
+ assert.equal(new Date(record.trainingTime).toISOString(), trainingTime.toISOString());
101
+ assert.equal(record.dossierFileIds?.length, 1);
102
+ assert.equal(record.dossierFiles?.length, 1);
103
+ assert.equal(String(recordDossierFile?.id), String(dossierFileAttendance.id));
104
+ assert.equal(recordDossierFile?.filename, 'attendance.txt');
105
+ assert.equal(recordDossierFile?.signed, true);
106
+ assert.equal(recordDossierFile?.meta?.category, 'attendanceSheet');
107
+ assert.equal(record.trainingRecordSubjects?.length, 1);
108
+ assert.equal(recordSubject?.name, '__Math__');
109
+ assert.equal(recordSubject?.score, 95);
110
+ const downloadResponse = await fetch(recordDossierFile.downloadUrl);
111
+ assert.equal(downloadResponse.ok, true);
112
+ assert.equal(await downloadResponse.text(), dossierTextAttendance.toString());
72
113
 
73
- let student: any = await app.bean.executor.performAction('get', '/training/student/:id', {
74
- params: { id: studentId },
75
- });
76
- let studentRecord = student.trainingRecords?.find(
77
- (item: any) => String(item.id) === String(recordId),
78
- );
79
- assert.equal(!!studentRecord, true);
80
- assert.equal(studentRecord?.trainingRecordSubjects?.length, 1);
114
+ let student: any = await app.bean.executor.performAction('get', '/training/student/:id', {
115
+ params: { id: studentId },
116
+ });
117
+ let studentRecord = student.trainingRecords?.find(
118
+ (item: any) => String(item.id) === String(recordId),
119
+ );
120
+ assert.equal(!!studentRecord, true);
121
+ assert.equal(studentRecord?.dossierFiles?.length, 1);
122
+ assert.equal(studentRecord?.trainingRecordSubjects?.length, 1);
81
123
 
82
- const dataUpdate = {
83
- studentId,
84
- name: '__RecordNew__',
85
- subjectCount: 2,
86
- totalScore: 183,
87
- averageScore: 91.5,
88
- trainingTime: trainingTimeUpdate,
89
- description: 'This is an updated record',
90
- trainingRecordSubjects: [
124
+ await fse.writeFile(dossierFilePathAssessment, dossierTextAssessment);
125
+ dossierFileAssessment = await app.bean.file.upload(
126
+ 'file-native:native',
91
127
  {
92
- id: recordSubject.id,
93
- name: '__MathNew__',
94
- score: 96,
95
- description: 'Updated math subject',
128
+ file: dossierFilePathAssessment,
129
+ filename: 'assessment.txt',
130
+ contentType: 'text/plain',
131
+ public: false,
132
+ meta: {
133
+ category: 'assessmentReport',
134
+ source: 'upload',
135
+ },
96
136
  },
97
137
  {
98
- name: '__English__',
99
- score: 87,
100
- description: 'English subject',
138
+ clientName: 'default',
139
+ fileScene: 'training-record:dossierFile',
101
140
  },
102
- ],
103
- } as any as DtoRecordUpdate;
104
- await app.bean.executor.performAction('patch', '/training/record/:id', {
105
- params: { id: recordId },
106
- body: dataUpdate,
107
- });
141
+ );
142
+ const dataUpdate = {
143
+ studentId,
144
+ name: '__RecordNew__',
145
+ subjectCount: 2,
146
+ totalScore: 183,
147
+ averageScore: 91.5,
148
+ trainingTime: trainingTimeUpdate,
149
+ dossierFileIds: [dossierFileAttendance.id, dossierFileAssessment.id],
150
+ description: 'This is an updated record',
151
+ trainingRecordSubjects: [
152
+ {
153
+ id: recordSubject.id,
154
+ name: '__MathNew__',
155
+ score: 96,
156
+ description: 'Updated math subject',
157
+ },
158
+ {
159
+ name: '__English__',
160
+ score: 87,
161
+ description: 'English subject',
162
+ },
163
+ ],
164
+ } as any as DtoRecordUpdate;
165
+ await app.bean.executor.performAction('patch', '/training/record/:id', {
166
+ params: { id: recordId },
167
+ body: dataUpdate,
168
+ });
108
169
 
109
- record = await app.bean.executor.performAction('get', '/training/record/:id', {
110
- params: { id: recordId },
111
- });
112
- const [updatedMathSubject, updatedEnglishSubject] = record.trainingRecordSubjects ?? [];
113
- assert.equal(record.name, dataUpdate.name);
114
- assert.equal(record.subjectCount, dataUpdate.subjectCount);
115
- assert.equal(record.totalScore, dataUpdate.totalScore);
116
- assert.equal(record.averageScore, dataUpdate.averageScore);
117
- assert.equal(new Date(record.trainingTime).toISOString(), trainingTimeUpdate.toISOString());
118
- assert.equal(record.trainingRecordSubjects?.length, 2);
119
- assert.equal(updatedMathSubject?.name, '__MathNew__');
120
- assert.equal(updatedMathSubject?.score, 96);
121
- assert.equal(updatedEnglishSubject?.name, '__English__');
122
- assert.equal(updatedEnglishSubject?.score, 87);
170
+ record = await app.bean.executor.performAction('get', '/training/record/:id', {
171
+ params: { id: recordId },
172
+ });
173
+ const [updatedMathSubject, updatedEnglishSubject] = record.trainingRecordSubjects ?? [];
174
+ assert.equal(record.name, dataUpdate.name);
175
+ assert.equal(record.subjectCount, dataUpdate.subjectCount);
176
+ assert.equal(record.totalScore, dataUpdate.totalScore);
177
+ assert.equal(record.averageScore, dataUpdate.averageScore);
178
+ assert.equal(new Date(record.trainingTime).toISOString(), trainingTimeUpdate.toISOString());
179
+ assert.equal(record.dossierFileIds?.length, 2);
180
+ assert.equal(record.dossierFiles?.length, 2);
181
+ assert.equal(
182
+ record.dossierFiles.some((item: any) => item.filename === 'attendance.txt'),
183
+ true,
184
+ );
185
+ assert.equal(
186
+ record.dossierFiles.some((item: any) => item.filename === 'assessment.txt'),
187
+ true,
188
+ );
189
+ assert.equal(record.trainingRecordSubjects?.length, 2);
190
+ assert.equal(updatedMathSubject?.name, '__MathNew__');
191
+ assert.equal(updatedMathSubject?.score, 96);
192
+ assert.equal(updatedEnglishSubject?.name, '__English__');
193
+ assert.equal(updatedEnglishSubject?.score, 87);
123
194
 
124
- student = await app.bean.executor.performAction('get', '/training/student/:id', {
125
- params: { id: studentId },
126
- });
127
- studentRecord = student.trainingRecords?.find(
128
- (item: any) => String(item.id) === String(recordId),
129
- );
130
- assert.equal(studentRecord?.name, dataUpdate.name);
131
- assert.equal(studentRecord?.trainingRecordSubjects?.length, 2);
195
+ student = await app.bean.executor.performAction('get', '/training/student/:id', {
196
+ params: { id: studentId },
197
+ });
198
+ studentRecord = student.trainingRecords?.find(
199
+ (item: any) => String(item.id) === String(recordId),
200
+ );
201
+ assert.equal(studentRecord?.name, dataUpdate.name);
202
+ assert.equal(studentRecord?.dossierFiles?.length, 2);
203
+ assert.equal(studentRecord?.trainingRecordSubjects?.length, 2);
132
204
 
133
- await app.bean.executor.performAction('delete', '/training/record/:id', {
134
- params: { id: recordId },
135
- });
205
+ await app.bean.executor.performAction('delete', '/training/record/:id', {
206
+ params: { id: recordId },
207
+ });
136
208
 
137
- record = await app.bean.executor.performAction('get', '/training/record/:id', {
138
- params: { id: recordId },
139
- });
140
- assert.equal(record, undefined);
209
+ record = await app.bean.executor.performAction('get', '/training/record/:id', {
210
+ params: { id: recordId },
211
+ });
212
+ assert.equal(record, undefined);
141
213
 
142
- student = await app.bean.executor.performAction('get', '/training/student/:id', {
143
- params: { id: studentId },
144
- });
145
- studentRecord = student.trainingRecords?.find(
146
- (item: any) => String(item.id) === String(recordId),
147
- );
148
- assert.equal(studentRecord, undefined);
214
+ student = await app.bean.executor.performAction('get', '/training/student/:id', {
215
+ params: { id: studentId },
216
+ });
217
+ studentRecord = student.trainingRecords?.find(
218
+ (item: any) => String(item.id) === String(recordId),
219
+ );
220
+ assert.equal(studentRecord, undefined);
149
221
 
150
- await app.bean.executor.performAction('delete', '/training/student/:id', {
151
- params: { id: studentId },
152
- });
222
+ const retainedDossierFileAttendance = await app.bean.file.get(dossierFileAttendance.id);
223
+ const retainedDossierFileAssessment = await app.bean.file.get(dossierFileAssessment.id);
224
+ assert.equal(!!retainedDossierFileAttendance, true);
225
+ assert.equal(!!retainedDossierFileAssessment, true);
153
226
 
154
- student = await app.bean.executor.performAction('get', '/training/student/:id', {
155
- params: { id: studentId },
156
- });
157
- assert.equal(student, undefined);
227
+ await app.bean.executor.performAction('delete', '/training/student/:id', {
228
+ params: { id: studentId },
229
+ });
158
230
 
159
- await app.bean.passport.signout();
231
+ student = await app.bean.executor.performAction('get', '/training/student/:id', {
232
+ params: { id: studentId },
233
+ });
234
+ assert.equal(student, undefined);
235
+ } finally {
236
+ if (dossierFileAssessment) {
237
+ await app.bean.file.delete(dossierFileAssessment.id);
238
+ }
239
+ if (dossierFileAttendance) {
240
+ await app.bean.file.delete(dossierFileAttendance.id);
241
+ }
242
+ await fse.remove(dossierFilePathAttendance);
243
+ await fse.remove(dossierFilePathAssessment);
244
+ await app.bean.passport.signout();
245
+ }
160
246
  });
161
247
  });
162
248
  });
@@ -9,6 +9,7 @@ export default {
9
9
  Student: 'Student',
10
10
  StudentName: 'Student Name',
11
11
  StudentImage: 'Student Image',
12
+ DossierFiles: 'Dossier Files',
12
13
  Summary: 'Summary',
13
14
  LevelTitle: 'Level Title',
14
15
  DescriptionLength: 'Description Length',
@@ -9,6 +9,7 @@ export default {
9
9
  Student: '学生',
10
10
  StudentName: '学生姓名',
11
11
  StudentImage: '学生图片',
12
+ DossierFiles: '培训档案附件',
12
13
  Summary: '摘要',
13
14
  LevelTitle: '阶段名称',
14
15
  DescriptionLength: '描述长度',
@@ -5,7 +5,12 @@ import { DtoImageView } from 'vona-module-a-image';
5
5
  import { $makeMetadata, Api, v } from 'vona-module-a-openapiutils';
6
6
  import { $Dto } from 'vona-module-a-orm';
7
7
  import { Dto } from 'vona-module-a-web';
8
- import { ModelRecord, onEffectForTrainingRecordSubjects } from 'vona-module-training-record';
8
+ import {
9
+ DtoRecordDossierFileView,
10
+ ModelRecord,
11
+ onEffectForTrainingRecordSubjects,
12
+ resolveDossierFiles,
13
+ } from 'vona-module-training-record';
9
14
  import { ZovaRender } from 'zova-rest-cabloy-basic-admin';
10
15
 
11
16
  import { $locale } from '../.metadata/locales.ts';
@@ -34,6 +39,7 @@ export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
34
39
  'averageScore',
35
40
  'trainingTime',
36
41
  'sceneImageIds',
42
+ 'dossierFileIds',
37
43
  'description',
38
44
  ],
39
45
  include: { trainingRecordSubjects: true },
@@ -48,4 +54,15 @@ export class DtoDetailRecordBase extends $Dto.get(() => ModelRecord, {
48
54
  v.array($Class.partial(DtoImageView)),
49
55
  )
50
56
  sceneImages?: Partial<DtoImageView>[];
57
+
58
+ @Api.field(
59
+ v.title($locale('DossierFiles')),
60
+ ZovaRender.visible(false),
61
+ v.optional(),
62
+ v.serializerTransform('a-serialization:custom', {
63
+ custom: resolveDossierFiles,
64
+ }),
65
+ v.array($Class.partial(DtoRecordDossierFileView)),
66
+ )
67
+ dossierFiles?: Partial<DtoRecordDossierFileView>[];
51
68
  }
@@ -65,8 +65,6 @@ export class EntityStudent extends EntityBase {
65
65
  ZovaRender.order(4),
66
66
  ZovaRender.field('basic-image:formFieldImage', {
67
67
  imageScene: 'training-student:studentImage',
68
- accept: ['image/png', 'image/jpeg', 'image/webp'],
69
- maxSize: 2 * 1024 * 1024,
70
68
  enableCrop: true,
71
69
  cropAspectRatio: 1,
72
70
  relationName: 'image',
@@ -18,6 +18,7 @@ export interface IModelOptionsStudent extends IDecoratorModelOptions<EntityStude
18
18
  'averageScore',
19
19
  'trainingTime',
20
20
  'sceneImageIds',
21
+ 'dossierFileIds',
21
22
  'description',
22
23
  ],
23
24
  }),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-file",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -55,11 +55,13 @@
55
55
  "fileProvider": {
56
56
  "optionsGlobalInterfaceName": "IDecoratorFileProviderOptions",
57
57
  "optionsGlobalInterfaceFrom": "vona-module-a-file",
58
- "scopeResource": true
58
+ "scopeResource": true,
59
+ "boilerplate": "fileProvider/boilerplate"
59
60
  },
60
61
  "fileScene": {
61
62
  "optionsGlobalInterfaceName": "IDecoratorFileSceneOptions",
62
- "optionsGlobalInterfaceFrom": "vona-module-a-file"
63
+ "optionsGlobalInterfaceFrom": "vona-module-a-file",
64
+ "boilerplate": "fileScene/boilerplate"
63
65
  }
64
66
  }
65
67
  }
@@ -125,6 +125,34 @@ export class BeanFileUploadPolicy extends BeanBase {
125
125
  };
126
126
  }
127
127
 
128
+ async resolveSceneUploadPolicy(data: { fileScene: keyof IFileSceneRecord }) {
129
+ const fileConfig = this.scope.config.file;
130
+ const fileScene = data.fileScene;
131
+ const sceneOptions = this._getSceneOptions(fileScene);
132
+ const { providerName, clientName } = await this._resolveProvider(sceneOptions);
133
+ const providerContext = await this.bean.fileProvider.getClientOptions({
134
+ providerName,
135
+ clientName,
136
+ });
137
+ if (!providerContext.entityFileProvider || providerContext.disabled) {
138
+ return this.app.throw(403, `File provider unavailable: ${providerName}.${clientName}`);
139
+ }
140
+ const uploadOptions = {
141
+ ...(fileConfig.upload ?? {}),
142
+ ...(sceneOptions.upload ?? {}),
143
+ };
144
+ const mimeTypes = [...(uploadOptions.mimeTypes ?? [])];
145
+ const extensions = [...(uploadOptions.extensions ?? [])];
146
+ return {
147
+ fileScene,
148
+ maxSize: uploadOptions.maxSize,
149
+ mimeTypes: mimeTypes.length > 0 ? mimeTypes : undefined,
150
+ extensions: extensions.length > 0 ? extensions : undefined,
151
+ multiple: uploadOptions.multiple,
152
+ public: this._resolvePublic(providerContext.clientOptions, sceneOptions),
153
+ };
154
+ }
155
+
128
156
  async resolveUploadPolicy(data: {
129
157
  fileScene: keyof IFileSceneRecord;
130
158
  size: number;
@@ -10,6 +10,8 @@ import { Arg, Controller, Web } from 'vona-module-a-web';
10
10
  import { DtoFileDirectUploadRequest } from '../dto/fileDirectUploadRequest.ts';
11
11
  import { DtoFileDirectUploadResponse } from '../dto/fileDirectUploadResponse.ts';
12
12
  import { DtoFileDownloadRequest } from '../dto/fileDownloadRequest.ts';
13
+ import { DtoFileUploadPolicyRequest } from '../dto/fileUploadPolicyRequest.ts';
14
+ import { DtoFileUploadPolicyResponse } from '../dto/fileUploadPolicyResponse.ts';
13
15
  import { DtoFileUploadResponse } from '../dto/fileUploadResponse.ts';
14
16
  import { DtoFileUploadTokenRequest } from '../dto/fileUploadTokenRequest.ts';
15
17
  import { DtoFileUploadTokenResponse } from '../dto/fileUploadTokenResponse.ts';
@@ -19,6 +21,12 @@ export interface IControllerOptionsFile extends IDecoratorControllerOptions {}
19
21
 
20
22
  @Controller<IControllerOptionsFile>({ path: 'file' })
21
23
  export class ControllerFile extends BeanBase {
24
+ @Web.post('upload-policy')
25
+ @Api.body(DtoFileUploadPolicyResponse)
26
+ async getUploadPolicy(@Arg.body() data: DtoFileUploadPolicyRequest) {
27
+ return await this.bean.fileUploadPolicy.resolveSceneUploadPolicy(data);
28
+ }
29
+
22
30
  @Web.post('upload-token')
23
31
  @Api.body(DtoFileUploadTokenResponse)
24
32
  async createUploadToken(@Arg.body() data: DtoFileUploadTokenRequest) {
@@ -0,0 +1,15 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { Api } from 'vona-module-a-openapiutils';
4
+ import { Dto } from 'vona-module-a-web';
5
+ import z from 'zod';
6
+
7
+ import type { IFileSceneRecord } from '../types/fileScene.ts';
8
+
9
+ export interface IDtoOptionsFileUploadPolicyRequest extends IDecoratorDtoOptions {}
10
+
11
+ @Dto<IDtoOptionsFileUploadPolicyRequest>()
12
+ export class DtoFileUploadPolicyRequest {
13
+ @Api.field(z.string())
14
+ fileScene: keyof IFileSceneRecord;
15
+ }
@@ -0,0 +1,30 @@
1
+ import type { IDecoratorDtoOptions } from 'vona-module-a-web';
2
+
3
+ import { Api, v } from 'vona-module-a-openapiutils';
4
+ import { Dto } from 'vona-module-a-web';
5
+ import z from 'zod';
6
+
7
+ import type { IFileSceneRecord } from '../types/fileScene.ts';
8
+
9
+ export interface IDtoOptionsFileUploadPolicyResponse extends IDecoratorDtoOptions {}
10
+
11
+ @Dto<IDtoOptionsFileUploadPolicyResponse>()
12
+ export class DtoFileUploadPolicyResponse {
13
+ @Api.field(z.string())
14
+ fileScene: keyof IFileSceneRecord;
15
+
16
+ @Api.field(v.optional(), z.number().int().positive())
17
+ maxSize?: number;
18
+
19
+ @Api.field(v.optional(), z.array(z.string()))
20
+ mimeTypes?: string[];
21
+
22
+ @Api.field(v.optional(), z.array(z.string()))
23
+ extensions?: string[];
24
+
25
+ @Api.field(v.optional(), z.boolean())
26
+ multiple?: boolean;
27
+
28
+ @Api.field(v.optional(), z.boolean())
29
+ public?: boolean;
30
+ }