cabloy 5.1.98 → 5.1.100

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 (141) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/hooks/contract-loop-gate.ts +151 -44
  3. package/CHANGELOG.md +28 -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 +56 -56
  27. package/vona/pnpm-workspace.yaml +1 -1
  28. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +22 -0
  29. package/vona/src/suite/a-training/modules/training-record/src/bean/meta.version.ts +1 -0
  30. package/vona/src/suite/a-training/modules/training-record/src/config/locale/en-us.ts +1 -0
  31. package/vona/src/suite/a-training/modules/training-record/src/config/locale/zh-cn.ts +1 -0
  32. package/vona/src/suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts +47 -0
  33. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +13 -0
  34. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +13 -0
  35. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +16 -5
  36. package/vona/src/suite/a-training/modules/training-record/src/index.ts +2 -0
  37. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +1 -0
  38. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +34 -0
  39. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +209 -123
  40. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  41. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  42. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +18 -1
  43. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +0 -2
  44. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +1 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +5 -3
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +28 -0
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +8 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyRequest.ts +15 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +30 -0
  50. package/vona/src/suite-vendor/a-file/package.json +2 -2
  51. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  52. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +27 -0
  53. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +8 -0
  54. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyRequest.ts +15 -0
  55. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +27 -0
  56. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +1 -1
  57. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +1 -1
  58. package/vona/src/suite-vendor/a-image/package.json +1 -1
  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-permission/package.json +1 -1
  61. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +24 -9
  62. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +87 -73
  63. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionActionByRoles.ts +27 -0
  64. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permissionUser.ts +20 -0
  65. package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
  66. package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +4 -3
  67. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  68. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +5 -1
  69. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  70. package/vona/src/suite-vendor/a-vona/modules/a-user/src/.metadata/index.ts +0 -13
  71. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +0 -7
  72. package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
  73. package/vona/src/suite-vendor/a-vona/modules/a-version/src/bean/meta.version.ts +2 -24
  74. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  75. package/zova/packages-utils/zova-jsx/package.json +2 -2
  76. package/zova/packages-zova/zova/package.json +3 -3
  77. package/zova/packages-zova/zova-core/package.json +1 -1
  78. package/zova/packages-zova/zova-core/src/bean/beanBase.ts +9 -13
  79. package/zova/pnpm-lock.yaml +24 -9
  80. package/zova/src/suite/a-demo/modules/demo-basic/src/page/toolOne/controller.tsx +2 -2
  81. package/zova/src/suite/a-demo/modules/demo-todo/src/page/todo/controller.tsx +2 -2
  82. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +2 -2
  83. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +2 -2
  84. package/zova/src/suite/a-home/modules/home-login/src/page/login/controller.tsx +2 -2
  85. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockDetails/controller.tsx +5 -1
  86. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/blockTable/controller.tsx +20 -15
  87. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +6 -1
  88. package/zova/src/suite/cabloy-basic/modules/basic-file/cli/openapi.config.ts +9 -0
  89. package/zova/src/suite/cabloy-basic/modules/basic-file/package.json +58 -0
  90. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/component/formFieldFile.ts +31 -0
  91. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +222 -0
  92. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/locales.ts +7 -0
  93. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/this.ts +2 -0
  94. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +121 -0
  95. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/baseURL.ts +5 -0
  96. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/index.ts +3 -0
  97. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +306 -0
  98. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +5718 -0
  99. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +40 -0
  100. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +187 -0
  101. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +534 -0
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/en-us.ts +15 -0
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/config/locale/zh-cn.ts +15 -0
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/index.ts +4 -0
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +74 -0
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/index.ts +1 -0
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/model/file.ts +23 -0
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/file.ts +12 -0
  109. package/zova/src/suite/cabloy-basic/modules/basic-file/src/types/index.ts +1 -0
  110. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.build.json +13 -0
  111. package/zova/src/suite/cabloy-basic/modules/basic-file/tsconfig.json +5 -0
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +61 -0
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +38 -1
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +31 -0
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +52 -0
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +614 -1
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +10 -0
  118. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +13 -0
  119. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +126 -53
  120. package/zova/src/suite/cabloy-basic/modules/basic-image/src/model/image.ts +23 -0
  121. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +44 -4
  122. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsx +17 -12
  123. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +2 -2
  124. package/zova/src/suite/cabloy-basic/modules/basic-resource/src/component/formFieldResourcePicker/controller.tsx +2 -2
  125. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/controller.tsx +7 -2
  126. package/zova/src/suite/cabloy-basic/modules/basic-table/src/component/table/render.tsx +34 -33
  127. package/zova/src/suite/cabloy-basic/package.json +1 -0
  128. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  129. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +5 -3
  130. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +2 -2
  131. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/resource/controller.tsx +2 -2
  132. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  133. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  134. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +17 -17
  135. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  136. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +8 -3
  137. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  138. package/zova/src/suite-vendor/a-zova/package.json +4 -4
  139. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/summerCache.permission.ts +0 -17
  140. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.userName.ts +0 -35
  141. /package/vona/patches/{zova-core@5.1.76.patch → zova-core@5.1.77.patch} +0 -0
@@ -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
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "vona-suite-a-file",
3
- "version": "5.0.3",
3
+ "version": "5.0.4",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "files": [],
7
7
  "type": "module",
8
8
  "dependencies": {
9
- "vona-module-a-file": "^5.0.3",
9
+ "vona-module-a-file": "^5.0.4",
10
10
  "vona-module-file-cloudflare": "^5.0.3",
11
11
  "vona-module-file-native": "^5.0.3"
12
12
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-image",
3
- "version": "5.0.4",
3
+ "version": "5.0.5",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -121,6 +121,33 @@ export class BeanImageUploadPolicy extends BeanBase {
121
121
  };
122
122
  }
123
123
 
124
+ async resolveSceneUploadPolicy(data: { imageScene: keyof IImageSceneRecord }) {
125
+ const imageConfig = this.scope.config.image;
126
+ const imageScene = data.imageScene;
127
+ const sceneOptions = this._getSceneOptions(imageScene);
128
+ const { providerName, clientName } = await this._resolveProvider(sceneOptions);
129
+ const { entityImageProvider, disabled } = await this.bean.imageProvider.getClientOptions({
130
+ providerName,
131
+ clientName,
132
+ });
133
+ if (!entityImageProvider || disabled) {
134
+ return this.app.throw(403, `Image provider unavailable: ${providerName}.${clientName}`);
135
+ }
136
+ const uploadOptions = {
137
+ ...(imageConfig.upload ?? {}),
138
+ ...(sceneOptions.upload ?? {}),
139
+ };
140
+ const mimeTypes = [...(uploadOptions.mimeTypes ?? [])];
141
+ const extensions = [...(uploadOptions.extensions ?? [])];
142
+ return {
143
+ imageScene,
144
+ maxSize: uploadOptions.maxSize,
145
+ mimeTypes: mimeTypes.length > 0 ? mimeTypes : undefined,
146
+ extensions: extensions.length > 0 ? extensions : undefined,
147
+ multiple: uploadOptions.multiple,
148
+ };
149
+ }
150
+
124
151
  async resolveUploadPolicy(data: {
125
152
  imageScene: keyof IImageSceneRecord;
126
153
  size: number;
@@ -12,6 +12,8 @@ import { DtoImageDirectUploadFinalizeRequest } from '../dto/imageDirectUploadFin
12
12
  import { DtoImageDirectUploadFinalizeResponse } from '../dto/imageDirectUploadFinalizeResponse.ts';
13
13
  import { DtoImageDirectUploadRequest } from '../dto/imageDirectUploadRequest.ts';
14
14
  import { DtoImageDirectUploadResponse } from '../dto/imageDirectUploadResponse.ts';
15
+ import { DtoImageUploadPolicyRequest } from '../dto/imageUploadPolicyRequest.ts';
16
+ import { DtoImageUploadPolicyResponse } from '../dto/imageUploadPolicyResponse.ts';
15
17
  import { DtoImageUploadResponse } from '../dto/imageUploadResponse.ts';
16
18
  import { DtoImageUploadTokenRequest } from '../dto/imageUploadTokenRequest.ts';
17
19
  import { DtoImageUploadTokenResponse } from '../dto/imageUploadTokenResponse.ts';
@@ -21,6 +23,12 @@ export interface IControllerOptionsImage extends IDecoratorControllerOptions {}
21
23
 
22
24
  @Controller<IControllerOptionsImage>({ path: 'image' })
23
25
  export class ControllerImage extends BeanBase {
26
+ @Web.post('upload-policy')
27
+ @Api.body(DtoImageUploadPolicyResponse)
28
+ async getUploadPolicy(@Arg.body() data: DtoImageUploadPolicyRequest) {
29
+ return await this.bean.imageUploadPolicy.resolveSceneUploadPolicy(data);
30
+ }
31
+
24
32
  @Web.post('upload-token')
25
33
  @Api.body(DtoImageUploadTokenResponse)
26
34
  async createUploadToken(@Arg.body() data: DtoImageUploadTokenRequest) {