cabloy 5.1.102 → 5.1.103

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 (127) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +29 -0
  3. package/CLAUDE.local.md +4 -0
  4. package/cabloy-docs/.vitepress/config.mjs +3 -0
  5. package/cabloy-docs/backend/file-guide.md +148 -0
  6. package/cabloy-docs/backend/image-guide.md +40 -40
  7. package/cabloy-docs/backend/jwt-guide.md +21 -0
  8. package/cabloy-docs/frontend/file-guide.md +147 -0
  9. package/cabloy-docs/frontend/image-guide.md +18 -9
  10. package/cabloy-docs/frontend/model-state-guide.md +12 -0
  11. package/cabloy-docs/frontend/use-state-data-best-practices.md +54 -0
  12. package/cabloy-docs/fullstack/file-workflow.md +135 -0
  13. package/cabloy-docs/fullstack/image-workflow.md +12 -3
  14. package/package.json +1 -1
  15. package/vona/packages-vona/vona/package.json +1 -1
  16. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +30 -4
  17. package/vona/src/suite/a-training/modules/training-record/src/bean/fileScene.dossierFile.ts +1 -1
  18. package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +1 -0
  19. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +8 -6
  20. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +8 -6
  21. package/vona/src/suite/a-training/modules/training-record/src/index.ts +0 -1
  22. package/vona/src/suite/a-training/modules/training-record/src/lib/index.ts +0 -1
  23. package/vona/src/suite/a-training/modules/training-record/test/record.test.ts +10 -12
  24. package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +1 -0
  25. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +9 -10
  26. package/vona/src/suite/a-training/modules/training-student/test/student.test.ts +2 -2
  27. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +3 -2
  28. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +155 -35
  29. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +283 -54
  30. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +46 -63
  31. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.index.ts +12 -0
  32. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +1 -1
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +3 -0
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/schedule.fileDraftPrune.ts +29 -0
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveView.ts +45 -0
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/serializerTransform.resolveViews.ts +45 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +6 -0
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +64 -56
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeRequest.ts +13 -0
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadFinalizeResponse.ts +10 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +0 -3
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +2 -14
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +3 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadPolicyResponse.ts +3 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +0 -25
  46. package/vona/src/{suite/a-training/modules/training-record/src/dto/recordDossierFileView.ts → suite-vendor/a-file/modules/a-file/src/dto/fileView.ts} +3 -15
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +10 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadUrl.ts +329 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +1 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +51 -10
  51. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +8 -3
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +1 -1
  53. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +1 -1
  54. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +12 -4
  55. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +52 -18
  56. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +1 -1
  57. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +5 -5
  58. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +7 -8
  59. package/vona/src/suite-vendor/a-file/package.json +4 -4
  60. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +1 -1
  61. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +16 -19
  62. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +99 -74
  63. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +28 -68
  64. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +1 -1
  65. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +3 -0
  66. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +3 -0
  67. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +4 -0
  68. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +78 -34
  69. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +3 -0
  70. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +0 -3
  71. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +1 -27
  72. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadPolicyResponse.ts +6 -0
  73. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +2 -36
  74. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +0 -3
  75. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +4 -27
  76. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +1 -1
  77. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +36 -29
  78. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +4 -4
  79. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +1 -0
  80. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  81. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +8 -11
  82. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +4 -16
  83. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +17 -16
  84. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +2 -6
  85. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +0 -37
  86. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +13 -36
  87. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +100 -113
  88. package/vona/src/suite-vendor/a-image/package.json +1 -1
  89. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  90. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/config/config.ts +3 -1
  91. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +9 -4
  92. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  93. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +113 -64
  94. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  95. package/zova/packages-zova/zova/package.json +2 -2
  96. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +50 -12
  97. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +610 -114
  98. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +44 -1
  99. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +50 -12
  100. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +610 -114
  101. package/zova/src/suite/cabloy-basic/modules/basic-file/src/.metadata/index.ts +16 -12
  102. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/file.ts +25 -25
  103. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/schemas.ts +18 -24
  104. package/zova/src/suite/cabloy-basic/modules/basic-file/src/api/openapi/types.ts +228 -291
  105. package/zova/src/suite/cabloy-basic/modules/basic-file/src/apiSchema/file.ts +5 -5
  106. package/zova/src/suite/cabloy-basic/modules/basic-file/src/bean/tableCell.file.tsx +23 -5
  107. package/zova/src/suite/cabloy-basic/modules/basic-file/src/component/formFieldFile/controller.tsx +72 -43
  108. package/zova/src/suite/cabloy-basic/modules/basic-file/src/lib/file.ts +22 -4
  109. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +16 -42
  110. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +5 -25
  111. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +10 -24
  112. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +216 -333
  113. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +0 -5
  114. package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +38 -12
  115. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +79 -32
  116. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +22 -4
  117. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +3 -4
  118. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  119. package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/utils.ts +29 -1
  120. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +2 -2
  121. package/zova/src/suite-vendor/a-zova/package.json +2 -2
  122. package/vona/src/suite/a-training/modules/training-record/src/lib/resolveDossierFiles.ts +0 -34
  123. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +0 -24
  124. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +0 -16
  125. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +0 -57
  126. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/imageNativeImage.ts +0 -31
  127. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/imageNativeImage.ts +0 -13
@@ -5,7 +5,6 @@ import { ApiSchema } from 'zova-module-a-api';
5
5
 
6
6
  import {
7
7
  ApiApiImagegetUploadPolicyPath,
8
- ApiApiImagecreateUploadTokenPath,
9
8
  ApiApiImageuploadPath,
10
9
  ApiApiImagecreateDirectUploadPath,
11
10
  ApiApiImagefinalizeDirectUploadPath,
@@ -19,10 +18,6 @@ export class ApiSchemaImage extends BeanBase {
19
18
  return this.$sdk.createApiSchemas(ApiApiImagegetUploadPolicyPath, 'post', options);
20
19
  }
21
20
 
22
- createUploadToken(options?: IApiSchemaOptions) {
23
- return this.$sdk.createApiSchemas(ApiApiImagecreateUploadTokenPath, 'post', options);
24
- }
25
-
26
21
  upload(options?: IApiSchemaOptions) {
27
22
  return this.$sdk.createApiSchemas(ApiApiImageuploadPath, 'post', options);
28
23
  }
@@ -7,7 +7,7 @@ import type {
7
7
  } from 'zova-module-a-table';
8
8
 
9
9
  import { classes } from 'typestyle';
10
- import { BeanBase } from 'zova';
10
+ import { BeanBase, ClientOnly } from 'zova';
11
11
  import { TableCell } from 'zova-module-a-table';
12
12
 
13
13
  import type { IImagePreviewItem } from '../lib/index.js';
@@ -105,6 +105,8 @@ export class TableCellImage extends BeanBase implements ITableCellRender {
105
105
  item: IImagePreviewItem,
106
106
  previewUrl: string,
107
107
  ): VNode {
108
+ const passportCode = this._getDeliveryPassportCode();
109
+ const src = passportCode ? this._resolvePreviewUrl(previewUrl, passportCode) : undefined;
108
110
  const size = options.size ?? 40;
109
111
  const style = {
110
112
  ...(options.style as any),
@@ -119,22 +121,30 @@ export class TableCellImage extends BeanBase implements ITableCellRender {
119
121
  )}
120
122
  style={style}
121
123
  >
122
- <img
123
- class="h-full w-full object-cover"
124
- style={{ objectFit: options.fit ?? 'cover' }}
125
- src={previewUrl}
126
- alt={item.filename ?? 'image'}
127
- />
124
+ <ClientOnly>
125
+ {src && (
126
+ <img
127
+ class="h-full w-full object-cover"
128
+ style={{ objectFit: options.fit ?? 'cover' }}
129
+ alt={item.filename ?? 'image'}
130
+ src={src}
131
+ />
132
+ )}
133
+ </ClientOnly>
128
134
  </div>
129
135
  );
130
136
  }
131
137
 
132
- private _openPreviewDialog(preview: IImagePreviewSummary, previewTitle: string) {
133
- const items = this._resolveDialogItems(preview);
138
+ private async _openPreviewDialog(preview: IImagePreviewSummary, previewTitle: string) {
139
+ const passportCode = await this._ensureDeliveryPassportCode();
140
+ const items = this._resolveDialogItems(preview).map(item => ({
141
+ ...item,
142
+ url: this._resolvePreviewUrl(item.url, passportCode),
143
+ }));
134
144
  openImagePreviewDialog({
135
145
  appModal: this.$appModal,
136
146
  title: previewTitle,
137
- items,
147
+ items: items.filter(item => !!item.url).map(item => ({ ...item, url: item.url! })),
138
148
  baseURL: this.sys.config.api.baseURL,
139
149
  });
140
150
  }
@@ -194,7 +204,23 @@ export class TableCellImage extends BeanBase implements ITableCellRender {
194
204
  return collectImageUrlPreviewItems(preview.source.value);
195
205
  }
196
206
 
197
- private _resolvePreviewUrl(url: string) {
198
- return resolveImagePreviewUrl(url, this.sys.config.api.baseURL);
207
+ private _getDeliveryPassportCode() {
208
+ const apiPrefix = this.sys.config.api.prefix ?? '/api';
209
+ return this.$passport.getFreshTempAuthToken({
210
+ path: `${apiPrefix}/image/delivery`,
211
+ staleTime: 30 * 1000,
212
+ });
213
+ }
214
+
215
+ private async _ensureDeliveryPassportCode() {
216
+ const apiPrefix = this.sys.config.api.prefix ?? '/api';
217
+ return await this.$passport.ensureFreshTempAuthToken({
218
+ path: `${apiPrefix}/image/delivery`,
219
+ staleTime: 30 * 1000,
220
+ });
221
+ }
222
+
223
+ private _resolvePreviewUrl(url: string, passportCode?: string) {
224
+ return resolveImagePreviewUrl(url, this.sys.config.api.baseURL, passportCode);
199
225
  }
200
226
  }
@@ -231,23 +231,24 @@ export class ControllerFormFieldImage extends BeanControllerBase {
231
231
  disableNotifyChanged?: boolean,
232
232
  ) {
233
233
  const previewUrl = item.url ? this._resolvePreviewUrl(item.url) : undefined;
234
+ const passportCode = this._getDeliveryPassportCode();
235
+ const src =
236
+ previewUrl && passportCode ? this._resolvePreviewUrl(previewUrl, passportCode) : undefined;
234
237
  return (
235
238
  <div
236
239
  key={`${item.id}-${index}`}
237
240
  class="rounded-box border border-base-300 bg-base-100 shadow-sm"
238
241
  >
239
242
  <div class="aspect-square overflow-hidden rounded-t-box bg-base-200">
240
- {previewUrl ? (
241
- <img
242
- class="h-full w-full object-cover"
243
- src={previewUrl}
244
- alt={item.filename ?? `image-${index + 1}`}
245
- />
246
- ) : (
247
- <div class="flex h-full w-full items-center justify-center text-sm text-base-content/50">
248
- #{String(item.id)}
249
- </div>
250
- )}
243
+ <ClientOnly>
244
+ {src && (
245
+ <img
246
+ class="h-full w-full object-cover"
247
+ alt={item.filename ?? `image-${index + 1}`}
248
+ src={src}
249
+ />
250
+ )}
251
+ </ClientOnly>
251
252
  </div>
252
253
  <div class="space-y-2 p-3">
253
254
  <div class="min-h-10 text-sm text-base-content/70">
@@ -365,15 +366,12 @@ export class ControllerFormFieldImage extends BeanControllerBase {
365
366
  }
366
367
  const preparedFile = await this._prepareFile(file, options);
367
368
  if (!preparedFile) continue;
368
- const tokenRes = await this.scope.api.image.createUploadToken({
369
- ...uploadTarget,
370
- size: preparedFile.size,
371
- mimeType: preparedFile.type || file.type,
372
- });
373
- const uploaded = await this.scope.api.image.upload({
374
- token: tokenRes.token,
375
- image: preparedFile,
376
- });
369
+ const uploaded = await this._uploadPreparedFile(
370
+ preparedFile,
371
+ file,
372
+ uploadTarget,
373
+ !!this._getCachedUploadPolicy(options)?.directUpload,
374
+ );
377
375
  const item: IImagePreviewItem = {
378
376
  id: uploaded.id,
379
377
  url: uploaded.url,
@@ -397,6 +395,40 @@ export class ControllerFormFieldImage extends BeanControllerBase {
397
395
  }
398
396
  }
399
397
 
398
+ private async _uploadPreparedFile(
399
+ preparedFile: File,
400
+ sourceFile: File,
401
+ uploadTarget: { imageScene: string },
402
+ directUpload: boolean,
403
+ ) {
404
+ if (!directUpload) {
405
+ return await this.scope.api.image.upload({
406
+ ...uploadTarget,
407
+ image: preparedFile,
408
+ });
409
+ }
410
+
411
+ const mimeType = preparedFile.type || sourceFile.type;
412
+ const directUploadRes = await this.scope.api.image.createDirectUpload({
413
+ ...uploadTarget,
414
+ filename: preparedFile.name,
415
+ size: preparedFile.size,
416
+ mimeType,
417
+ });
418
+ const formData = new FormData();
419
+ formData.append('file', preparedFile);
420
+ const providerRes = await fetch(directUploadRes.uploadUrl, {
421
+ method: 'POST',
422
+ body: formData,
423
+ });
424
+ if (!providerRes.ok) {
425
+ throw new Error(`image direct upload failed: ${providerRes.status}`);
426
+ }
427
+ return await this.scope.api.image.finalizeDirectUpload({
428
+ imageId: directUploadRes.id,
429
+ });
430
+ }
431
+
400
432
  private _resolveUploadTarget() {
401
433
  const imageScene = this.currentOptions.imageScene;
402
434
  if (!imageScene) {
@@ -794,26 +826,25 @@ export class ControllerFormFieldImage extends BeanControllerBase {
794
826
  });
795
827
  }
796
828
 
797
- private _openPreviewDialog(items: IImagePreviewItem[], currentIndex: number) {
829
+ private async _openPreviewDialog(items: IImagePreviewItem[], currentIndex: number) {
830
+ const passportCode = await this._ensureDeliveryPassportCode();
798
831
  const previewItems = items
799
- .map((item, index) => ({
800
- index,
801
- item,
802
- }))
832
+ .map((item, index) => ({ index, item }))
803
833
  .filter(({ item }) => !!item.url)
804
834
  .map(({ index, item }) => ({
805
835
  index,
806
836
  item: {
807
- url: item.url!,
837
+ url: this._resolvePreviewUrl(item.url!, passportCode),
808
838
  filename: item.filename,
809
839
  },
810
840
  }));
811
- if (previewItems.length === 0) return;
812
- const initialIndex = previewItems.findIndex(({ index }) => index === currentIndex);
841
+ const resolvedItems = previewItems.filter(item => !!item.item.url);
842
+ if (resolvedItems.length === 0) return;
843
+ const initialIndex = resolvedItems.findIndex(({ index }) => index === currentIndex);
813
844
  openImagePreviewDialog({
814
845
  appModal: this.$appModal,
815
- title: this._getPreviewDialogTitle(previewItems.length),
816
- items: previewItems.map(({ item }) => item),
846
+ title: this._getPreviewDialogTitle(resolvedItems.length),
847
+ items: resolvedItems.map(({ item }) => ({ ...item, url: item.url! })),
817
848
  initialIndex: initialIndex === -1 ? 0 : initialIndex,
818
849
  baseURL: this.sys.config.api.baseURL,
819
850
  });
@@ -829,8 +860,24 @@ export class ControllerFormFieldImage extends BeanControllerBase {
829
860
  );
830
861
  }
831
862
 
832
- private _resolvePreviewUrl(url: string) {
833
- return resolveImagePreviewUrl(url, this.sys.config.api.baseURL);
863
+ private _getDeliveryPassportCode() {
864
+ const apiPrefix = this.sys.config.api.prefix ?? '/api';
865
+ return this.$passport.getFreshTempAuthToken({
866
+ path: `${apiPrefix}/image/delivery`,
867
+ staleTime: 30 * 1000,
868
+ });
869
+ }
870
+
871
+ private async _ensureDeliveryPassportCode() {
872
+ const apiPrefix = this.sys.config.api.prefix ?? '/api';
873
+ return await this.$passport.ensureFreshTempAuthToken({
874
+ path: `${apiPrefix}/image/delivery`,
875
+ staleTime: 30 * 1000,
876
+ });
877
+ }
878
+
879
+ private _resolvePreviewUrl(url: string, passportCode?: string) {
880
+ return resolveImagePreviewUrl(url, this.sys.config.api.baseURL, passportCode);
834
881
  }
835
882
 
836
883
  private _formatBytes(bytes: number) {
@@ -1,3 +1,5 @@
1
+ import { $protocolKey } from 'zova';
2
+
1
3
  export interface IImagePreviewItemBase {
2
4
  url?: string;
3
5
  filename?: string;
@@ -19,10 +21,26 @@ export function inferImageRelationName(fieldName?: string, relationName?: string
19
21
  return undefined;
20
22
  }
21
23
 
22
- export function resolveImagePreviewUrl(url: string | undefined, baseURL?: string) {
23
- if (!url || !url.startsWith('/api/')) return url;
24
- if (!baseURL) return url;
25
- return `${baseURL.replace(/\/$/, '')}${url}`;
24
+ export function resolveImagePreviewUrl(
25
+ url: string | undefined,
26
+ baseURL?: string,
27
+ passportCode?: string,
28
+ ) {
29
+ if (!url) return url;
30
+ const resolvedUrl =
31
+ url.startsWith('/api/') && baseURL ? `${baseURL.replace(/\/$/, '')}${url}` : url;
32
+ if (!passportCode) return resolvedUrl;
33
+ const apiBaseUrl = new URL(baseURL ?? 'http://localhost', 'http://localhost');
34
+ const parsedUrl = new URL(resolvedUrl, apiBaseUrl);
35
+ if (parsedUrl.origin !== apiBaseUrl.origin || !parsedUrl.pathname.startsWith('/api/')) {
36
+ return resolvedUrl;
37
+ }
38
+ const passportCodeKey = $protocolKey('x-vona-passport-code');
39
+ if (!parsedUrl.searchParams.has(passportCodeKey)) {
40
+ parsedUrl.searchParams.set(passportCodeKey, passportCode);
41
+ }
42
+ if (!baseURL) return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`;
43
+ return parsedUrl.toString();
26
44
  }
27
45
 
28
46
  export function buildImagePreviewTitle(
@@ -1,5 +1,4 @@
1
- import type { components } from '../api/openapi/index.js';
1
+ import type { ApiSchemaAImageDtoImageTransformOptions_2d063d28bc7243bed02ebd8bddf1212a93c6305b } from '../api/openapi/schemas.js';
2
2
 
3
- export type IImageTransformOptions = NonNullable<
4
- NonNullable<components['schemas']['a-image.dto.imageUploadResponse']['variants']>[string]
5
- >;
3
+ export type IImageTransformOptions =
4
+ NonNullable<ApiSchemaAImageDtoImageTransformOptions_2d063d28bc7243bed02ebd8bddf1212a93c6305b>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-module-a-model",
3
- "version": "5.1.31",
3
+ "version": "5.1.32",
4
4
  "gitHead": "09d901d17140a80ee0764211b441cda72fd94663",
5
5
  "description": "core",
6
6
  "keywords": [
@@ -1,6 +1,34 @@
1
1
  import type { DataQuery } from '../types/query.js';
2
2
 
3
- export type TypeQueryEnsureLoadedFn<T> = () => DataQuery<T> | undefined;
3
+ export type TypeQueryEnsureLoadedFn<TData, TError = Error | null> = () =>
4
+ | DataQuery<TData, TError>
5
+ | undefined;
6
+
7
+ export type TypeQueryIsStaleFn<TData, TError = Error | null> = (
8
+ query: DataQuery<TData, TError>,
9
+ ) => boolean;
10
+
11
+ export function $QueryGetFresh<TData = any, TError = Error | null>(
12
+ fn: TypeQueryEnsureLoadedFn<TData, TError>,
13
+ isStale: TypeQueryIsStaleFn<TData, TError>,
14
+ ): TData | undefined {
15
+ const query = fn();
16
+ if (!query) return;
17
+ if (!isStale(query)) return query.data;
18
+ void query.suspense();
19
+ }
20
+
21
+ export async function $QueryEnsureFresh<TData = any, TError = Error | null>(
22
+ fn: TypeQueryEnsureLoadedFn<TData, TError>,
23
+ isStale: TypeQueryIsStaleFn<TData, TError>,
24
+ ): Promise<TData | undefined> {
25
+ const query = fn();
26
+ if (!query) return;
27
+ if (!isStale(query)) return query.data;
28
+ const result = await query.suspense();
29
+ if (result.error) throw result.error;
30
+ return result.data;
31
+ }
4
32
 
5
33
  export async function $QueryEnsureLoaded<T = any>(fn: TypeQueryEnsureLoadedFn<T>) {
6
34
  return _QueryEnsureLoadedInner<T>(fn);
@@ -108,8 +108,8 @@ export interface QueryMetaPersister {
108
108
  refetchOnRestore?: boolean | 'always';
109
109
  }
110
110
 
111
- export type DataQuery<TData> = UnwrapNestedRefs<
112
- ReturnType<typeof useQuery<TData | undefined, Error | null>>
111
+ export type DataQuery<TData, TError = Error | null> = UnwrapNestedRefs<
112
+ ReturnType<typeof useQuery<TData | undefined, TError>>
113
113
  >;
114
114
 
115
115
  export type DataMutation<TData = unknown, TVariables = void, TContext = unknown> = UnwrapNestedRefs<
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zova-suite-a-zova",
3
- "version": "5.1.131",
3
+ "version": "5.1.132",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "zova",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "zova-module-a-interceptor": "^5.1.29",
22
22
  "zova-module-a-logger": "^5.1.26",
23
23
  "zova-module-a-meta": "^5.1.21",
24
- "zova-module-a-model": "^5.1.31",
24
+ "zova-module-a-model": "^5.1.32",
25
25
  "zova-module-a-openapi": "^5.1.43",
26
26
  "zova-module-a-router": "^5.1.29",
27
27
  "zova-module-a-routerstack": "^5.1.26",
@@ -1,34 +0,0 @@
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,24 +0,0 @@
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 { IImageSceneRecord } from '../types/imageScene.ts';
8
-
9
- export interface IDtoOptionsImageUploadTokenRequest extends IDecoratorDtoOptions {}
10
-
11
- @Dto<IDtoOptionsImageUploadTokenRequest>()
12
- export class DtoImageUploadTokenRequest {
13
- @Api.field(z.string())
14
- imageScene: keyof IImageSceneRecord;
15
-
16
- @Api.field(z.number().int().positive())
17
- size: number;
18
-
19
- @Api.field(z.string())
20
- mimeType: string;
21
-
22
- @Api.field(v.optional(), z.number().int().positive())
23
- expiresIn?: number;
24
- }
@@ -1,16 +0,0 @@
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
- export interface IDtoOptionsImageUploadTokenResponse extends IDecoratorDtoOptions {}
8
-
9
- @Dto<IDtoOptionsImageUploadTokenResponse>()
10
- export class DtoImageUploadTokenResponse {
11
- @Api.field(z.string())
12
- token: string;
13
-
14
- @Api.field(v.optional(), z.number().int().positive())
15
- expiresIn?: number;
16
- }
@@ -1,57 +0,0 @@
1
- import type { IUploadFile } from 'vona-module-a-upload';
2
- import type { IDecoratorControllerOptions } from 'vona-module-a-web';
3
-
4
- import fse from 'fs-extra';
5
- import { BeanBase } from 'vona';
6
- import { Core } from 'vona-module-a-core';
7
- import { Passport } from 'vona-module-a-user';
8
- import { Arg, Controller, Web } from 'vona-module-a-web';
9
-
10
- export interface IControllerOptionsImage extends IDecoratorControllerOptions {}
11
-
12
- @Controller<IControllerOptionsImage>({ path: 'image-native' })
13
- export class ControllerImage extends BeanBase {
14
- @Web.post('direct-upload/:resourceId')
15
- @Passport.public()
16
- @Core.fileUpload()
17
- async directUpload(
18
- @Arg.param('resourceId') resourceId: string,
19
- @Arg.query('token') token: string,
20
- @Arg.file('image') file: IUploadFile,
21
- ) {
22
- const payload = await this.bean.imageUploadPolicy.verifyDirectUploadToken(
23
- token,
24
- this.ctx.route.routePathRaw,
25
- );
26
- if (payload.resourceId !== resourceId) {
27
- return this.app.throw(401);
28
- }
29
- const modelImage = this.app.bean._getBean<any>('a-image.model.image' as never);
30
- const image = await modelImage.get({
31
- providerName: 'image-native:native',
32
- resourceId,
33
- });
34
- if (!image || image.status !== 'draft') {
35
- return this.app.throw(403, `image is not draft: ${resourceId}`);
36
- }
37
- if (!image.imageScene) {
38
- throw new Error(`image scene missing: ${resourceId}`);
39
- }
40
- const stat = await fse.stat(file.file);
41
- const policy = await this.bean.imageUploadPolicy.resolveUploadPolicy({
42
- imageScene: image.imageScene as never,
43
- size: Number(stat.size),
44
- mimeType: file.info.mimeType,
45
- });
46
- await this.bean.imageUploadPolicy.validateUploadFile(
47
- {
48
- file: file.file,
49
- filename: file.info.filename,
50
- mimeType: file.info.mimeType,
51
- },
52
- policy,
53
- );
54
- await this.scope.service.imageNative.uploadDirectFile(image, file);
55
- return { ok: true };
56
- }
57
- }
@@ -1,31 +0,0 @@
1
- import type { IApiActionOptions } from 'zova-module-a-api';
2
-
3
- import { Api, BeanApiBase } from 'zova-module-a-api';
4
-
5
- import type { paths } from './openapi/index.js';
6
-
7
- import { OpenApiBaseURL } from './openapi/index.js';
8
-
9
- /** ImageNativeImage_directUpload */
10
- export const ApiApiImageNativeImagedirectUploadPath =
11
- '/api/image/native/image-native/direct-upload/{resourceId}';
12
- export type ApiApiImageNativeImagedirectUploadPath =
13
- '/api/image/native/image-native/direct-upload/{resourceId}';
14
- export type ApiApiImageNativeImagedirectUploadMethod = 'post';
15
- export type ApiApiImageNativeImagedirectUploadRequestBody = {
16
- /** Format: binary */
17
- image: Blob;
18
- };
19
- export type ApiApiImageNativeImagedirectUploadResponseBody =
20
- paths[ApiApiImageNativeImagedirectUploadPath][ApiApiImageNativeImagedirectUploadMethod]['responses']['200']['content']['application/json']['data'];
21
-
22
- @Api()
23
- export class ApiImageNativeImage extends BeanApiBase {
24
- directUpload(body: ApiApiImageNativeImagedirectUploadRequestBody, options?: IApiActionOptions) {
25
- return this.$fetch.post<any, ApiApiImageNativeImagedirectUploadResponseBody>(
26
- ApiApiImageNativeImagedirectUploadPath,
27
- this.$formData(body),
28
- this.$configPrepare(OpenApiBaseURL(this.sys), options),
29
- );
30
- }
31
- }
@@ -1,13 +0,0 @@
1
- import type { IApiSchemaOptions } from 'zova-module-a-api';
2
-
3
- import { BeanBase } from 'zova';
4
- import { ApiSchema } from 'zova-module-a-api';
5
-
6
- import { ApiApiImageNativeImagedirectUploadPath } from '../api/imageNativeImage.js';
7
-
8
- @ApiSchema()
9
- export class ApiSchemaImageNativeImage extends BeanBase {
10
- directUpload(options?: IApiSchemaOptions) {
11
- return this.$sdk.createApiSchemas(ApiApiImageNativeImagedirectUploadPath, 'post', options);
12
- }
13
- }