cabloy 5.1.95 → 5.1.97

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 (208) hide show
  1. package/.cabloy-version +1 -0
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +4 -1
  3. package/.claude/skills/cabloy-resource-field-update/SKILL.md +6 -3
  4. package/.github/workflows/vona-test-crud.yml +2 -0
  5. package/.github/workflows/vona-test-mysql.yml +2 -0
  6. package/.github/workflows/vona-test-pg.yml +2 -0
  7. package/.github/workflows/vona-test-sqlite3.yml +2 -0
  8. package/CHANGELOG.md +38 -0
  9. package/CLAUDE.md +1 -0
  10. package/cabloy-docs/ai/playbook-backend-module.md +6 -3
  11. package/cabloy-docs/fullstack/framework-performance.md +3 -3
  12. package/cabloy-docs/fullstack/quickstart.md +1 -1
  13. package/package.json +1 -1
  14. package/scripts/release.ts +27 -1
  15. package/vona/docker-compose-original/docker-compose-dockerfile-app +4 -0
  16. package/vona/docker-compose-original/docker-compose.original.yml +0 -1
  17. package/vona/env/.env +0 -1
  18. package/vona/env/.env.prod +0 -1
  19. package/vona/env/.env.prod.docker +0 -7
  20. package/vona/package.original.json +3 -3
  21. package/vona/packages-cli/cabloy-cli/package.json +2 -2
  22. package/vona/packages-cli/cli/package.json +1 -1
  23. package/vona/packages-cli/cli-set-api/package.json +2 -2
  24. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +70 -2
  25. package/vona/packages-utils/babel-plugin-vona-bean-module/package.json +2 -2
  26. package/vona/packages-utils/module-glob/package.json +2 -2
  27. package/vona/packages-utils/module-info-pro/package.json +2 -2
  28. package/vona/packages-utils/utils/package.json +2 -2
  29. package/vona/packages-vona/vona/package.json +1 -1
  30. package/vona/packages-vona/vona-core/package.json +2 -2
  31. package/vona/packages-vona/vona-core/src/lib/addon/addon.ts +77 -0
  32. package/vona/packages-vona/vona-core/src/lib/addon/index.ts +3 -0
  33. package/vona/packages-vona/vona-core/src/lib/addon/sharp.ts +46 -0
  34. package/vona/packages-vona/vona-core/src/lib/addon/sqlite3.ts +23 -0
  35. package/vona/packages-vona/vona-core/src/lib/index.ts +1 -0
  36. package/vona/packages-vona/vona-core/src/lib/utils/index.ts +0 -1
  37. package/vona/packages-vona/vona-core/src/lib/utils/util.ts +5 -0
  38. package/vona/packages-vona/vona-mock/package.json +2 -2
  39. package/vona/pnpm-lock.yaml +441 -62
  40. package/vona/pnpm-workspace.yaml +1 -1
  41. package/vona/src/backend/cli.ts +2 -5
  42. package/vona/src/backend/config/config/config.ts +0 -4
  43. package/vona/src/suite/a-demo/modules/demo-basic/src/bean/ssrMenu.admin.ts +1 -1
  44. package/vona/src/suite/a-training/modules/training-record/src/.metadata/index.ts +26 -0
  45. package/vona/src/suite/a-training/modules/training-record/src/bean/imageScene.sceneImage.ts +11 -0
  46. package/vona/src/suite/a-training/modules/training-record/src/bean/meta.version.ts +1 -0
  47. package/vona/src/suite/a-training/modules/training-record/src/config/locale/en-us.ts +1 -0
  48. package/vona/src/suite/a-training/modules/training-record/src/config/locale/zh-cn.ts +1 -0
  49. package/vona/src/suite/a-training/modules/training-record/src/dto/recordSelectResItem.tsx +12 -0
  50. package/vona/src/suite/a-training/modules/training-record/src/dto/recordView.tsx +12 -0
  51. package/vona/src/suite/a-training/modules/training-record/src/entity/record.tsx +26 -1
  52. package/vona/src/suite/a-training/modules/training-record/src/index.ts +1 -0
  53. package/vona/src/suite/a-training/modules/training-student/src/.metadata/index.ts +27 -1
  54. package/vona/src/suite/a-training/modules/training-student/src/bean/imageScene.studentImage.ts +10 -0
  55. package/vona/src/suite/a-training/modules/training-student/src/bean/meta.version.ts +1 -0
  56. package/vona/src/suite/a-training/modules/training-student/src/config/locale/en-us.ts +1 -0
  57. package/vona/src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts +1 -0
  58. package/vona/src/suite/a-training/modules/training-student/src/dto/detailRecordBase.tsx +16 -2
  59. package/vona/src/suite/a-training/modules/training-student/src/dto/studentCreate.tsx +1 -1
  60. package/vona/src/suite/a-training/modules/training-student/src/dto/studentSelectResItem.tsx +12 -0
  61. package/vona/src/suite/a-training/modules/training-student/src/dto/studentUpdate.tsx +1 -1
  62. package/vona/src/suite/a-training/modules/training-student/src/dto/studentView.tsx +13 -1
  63. package/vona/src/suite/a-training/modules/training-student/src/entity/student.tsx +26 -1
  64. package/vona/src/suite/a-training/modules/training-student/src/index.ts +1 -0
  65. package/vona/src/suite/a-training/modules/training-student/src/model/student.ts +1 -0
  66. package/vona/src/suite-vendor/a-auth/modules/a-auth/package.json +1 -1
  67. package/vona/src/suite-vendor/a-auth/modules/a-auth/src/bean/bean.authProvider.ts +2 -5
  68. package/vona/src/suite-vendor/a-auth/package.json +1 -1
  69. package/vona/src/suite-vendor/a-image/modules/a-image/cli/imageProvider/boilerplate/{{sceneName}}.{{beanName}}.ts_ +23 -0
  70. package/vona/src/suite-vendor/a-image/modules/a-image/cli/imageScene/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -0
  71. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +68 -0
  72. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +482 -0
  73. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/locales.ts +18 -0
  74. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/this.ts +2 -0
  75. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +443 -0
  76. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageProvider.ts +55 -0
  77. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +174 -0
  78. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.redlock.ts +11 -0
  79. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +40 -0
  80. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveView.ts +44 -0
  81. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/serializerTransform.resolveViews.ts +44 -0
  82. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +17 -0
  83. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/locale/en-us.ts +1 -0
  84. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/locale/zh-cn.ts +1 -0
  85. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +165 -0
  86. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDeliveryRequest.ts +20 -0
  87. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadRequest.ts +36 -0
  88. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +38 -0
  89. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageTransformOptions.tsx +42 -0
  90. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +62 -0
  91. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenRequest.ts +24 -0
  92. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadTokenResponse.ts +16 -0
  93. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadUrlRequest.ts +33 -0
  94. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +46 -0
  95. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +58 -0
  96. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/imageProvider.ts +24 -0
  97. package/vona/src/suite-vendor/a-image/modules/a-image/src/index.ts +4 -0
  98. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageProvider.ts +9 -0
  99. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageScene.ts +7 -0
  100. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageVariant.ts +46 -0
  101. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/index.ts +3 -0
  102. package/vona/src/suite-vendor/a-image/modules/a-image/src/model/image.ts +10 -0
  103. package/vona/src/suite-vendor/a-image/modules/a-image/src/model/imageProvider.ts +10 -0
  104. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +208 -0
  105. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +110 -0
  106. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageScene.ts +52 -0
  107. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/index.ts +3 -0
  108. package/vona/src/suite-vendor/a-image/modules/a-image/tsconfig.build.json +11 -0
  109. package/vona/src/suite-vendor/a-image/modules/a-image/tsconfig.json +7 -0
  110. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +49 -0
  111. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/.metadata/index.ts +89 -0
  112. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/.metadata/this.ts +2 -0
  113. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +155 -0
  114. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/index.ts +1 -0
  115. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/lib/cloudflareImageUrl.ts +112 -0
  116. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +273 -0
  117. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/tsconfig.build.json +11 -0
  118. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/tsconfig.json +7 -0
  119. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +63 -0
  120. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +114 -0
  121. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/this.ts +2 -0
  122. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +131 -0
  123. package/vona/src/suite-vendor/a-image/modules/image-native/src/config/config.ts +11 -0
  124. package/vona/src/suite-vendor/a-image/modules/image-native/src/index.ts +1 -0
  125. package/vona/src/suite-vendor/a-image/modules/image-native/src/monkey.ts +33 -0
  126. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +291 -0
  127. package/vona/src/suite-vendor/a-image/modules/image-native/tsconfig.build.json +11 -0
  128. package/vona/src/suite-vendor/a-image/modules/image-native/tsconfig.json +7 -0
  129. package/vona/src/suite-vendor/a-image/package.json +14 -0
  130. package/vona/src/suite-vendor/a-image/tsconfig.base.json +4 -0
  131. package/vona/src/suite-vendor/a-image/tsconfig.json +16 -0
  132. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +2 -2
  133. package/vona/src/suite-vendor/a-vona/modules/a-jwt/package.json +1 -1
  134. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/service/jwtClient.ts +5 -0
  135. package/vona/src/suite-vendor/a-vona/modules/a-jwt/src/types/jwt.ts +1 -0
  136. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  137. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/schema/makeSchemaLikes.ts +6 -3
  138. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +13 -7
  139. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  140. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/database.ts +0 -1
  141. package/vona/src/suite-vendor/a-vona/modules/a-ormdialect/package.json +9 -1
  142. package/vona/src/suite-vendor/a-vona/modules/a-upload/package.json +1 -1
  143. package/vona/src/suite-vendor/a-vona/modules/a-upload/src/bean/interceptor.upload.ts +39 -7
  144. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  145. package/zova/packages-cli/cli/package.json +3 -3
  146. package/zova/packages-cli/cli-set-front/package.json +9 -9
  147. package/zova/packages-cli/cli-set-front/src/lib/bean/cli.openapi.generate.ts +20 -10
  148. package/zova/packages-utils/babel-plugin-zova-bean-module/package.json +2 -2
  149. package/zova/packages-utils/babel-plugin-zova-bean-use/package.json +2 -2
  150. package/zova/packages-utils/babel-plugin-zova-behavior/package.json +2 -2
  151. package/zova/packages-utils/babel-plugin-zova-component/package.json +2 -2
  152. package/zova/packages-utils/zova-jsx/package.json +3 -3
  153. package/zova/packages-utils/zova-vite/package.json +7 -7
  154. package/zova/packages-zova/zova/package.json +3 -3
  155. package/zova/packages-zova/zova-core/package.json +3 -3
  156. package/zova/pnpm-lock.yaml +126 -74
  157. package/zova/src/suite/a-home/modules/home-api/src/api/captcha.ts +3 -1
  158. package/zova/src/suite/a-home/modules/home-api/src/api/home.ts +3 -1
  159. package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseMenu.ts +3 -1
  160. package/zova/src/suite/a-home/modules/home-api/src/api/homeBasePermission.ts +3 -1
  161. package/zova/src/suite/a-home/modules/home-api/src/api/homeUserPassport.ts +22 -20
  162. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +163 -53
  163. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +2060 -675
  164. package/zova/src/suite/a-home/modules/home-api/src/api/testSsrToolOne.ts +3 -1
  165. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/captcha.ts +3 -1
  166. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/home.ts +3 -1
  167. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseMenu.ts +3 -1
  168. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBasePermission.ts +3 -1
  169. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeUserPassport.ts +13 -11
  170. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/testSsrToolOne.ts +3 -1
  171. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +54 -16
  172. package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +468 -8
  173. package/zova/src/suite/a-training/modules/training-student/src/apiSchema/trainingStudent.ts +3 -1
  174. package/zova/src/suite/cabloy-basic/modules/basic-details/src/bean/tableCell.actionUpdate.tsx +1 -1
  175. package/zova/src/suite/cabloy-basic/modules/basic-details/src/component/actionCreate/controller.tsx +1 -1
  176. package/zova/src/suite/cabloy-basic/modules/basic-details/src/service/detail.tsx +53 -1
  177. package/zova/src/suite/cabloy-basic/modules/basic-image/cli/openapi.config.ts +9 -0
  178. package/zova/src/suite/cabloy-basic/modules/basic-image/package.json +60 -0
  179. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/component/formFieldImage.ts +31 -0
  180. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/index.ts +191 -0
  181. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/locales.ts +7 -0
  182. package/zova/src/suite/cabloy-basic/modules/basic-image/src/.metadata/this.ts +2 -0
  183. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/image.ts +104 -0
  184. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/baseURL.ts +5 -0
  185. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/index.ts +3 -0
  186. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/schemas.ts +262 -0
  187. package/zova/src/suite/cabloy-basic/modules/basic-image/src/api/openapi/types.ts +5159 -0
  188. package/zova/src/suite/cabloy-basic/modules/basic-image/src/apiSchema/image.ts +35 -0
  189. package/zova/src/suite/cabloy-basic/modules/basic-image/src/bean/tableCell.image.tsx +200 -0
  190. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +781 -0
  191. package/zova/src/suite/cabloy-basic/modules/basic-image/src/config/locale/en-us.ts +18 -0
  192. package/zova/src/suite/cabloy-basic/modules/basic-image/src/config/locale/zh-cn.ts +18 -0
  193. package/zova/src/suite/cabloy-basic/modules/basic-image/src/index.ts +4 -0
  194. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/index.ts +2 -0
  195. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/preview.ts +105 -0
  196. package/zova/src/suite/cabloy-basic/modules/basic-image/src/lib/previewDialog.tsx +190 -0
  197. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/image.ts +5 -0
  198. package/zova/src/suite/cabloy-basic/modules/basic-image/src/types/index.ts +1 -0
  199. package/zova/src/suite/cabloy-basic/modules/basic-image/tsconfig.build.json +13 -0
  200. package/zova/src/suite/cabloy-basic/modules/basic-image/tsconfig.json +5 -0
  201. package/zova/src/suite/cabloy-basic/package.json +1 -0
  202. package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
  203. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/image.ts +1 -0
  204. package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/index.ts +1 -0
  205. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +5 -5
  206. package/zova/src/suite-vendor/a-zova/package.json +3 -3
  207. package/vona/packages-vona/vona-core/src/lib/utils/sqlite3.ts +0 -50
  208. /package/vona/patches/{zova-core@5.1.75.patch → zova-core@5.1.76.patch} +0 -0
@@ -0,0 +1,443 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+
3
+ import { BeanBase } from 'vona';
4
+ import { Bean } from 'vona-module-a-bean';
5
+
6
+ import type { EntityImage } from '../entity/image.ts';
7
+ import type {
8
+ IImageDeliveryOptions,
9
+ IImageDirectUploadInput,
10
+ IImageDirectUploadResult,
11
+ IImageProviderDirectUploadResource,
12
+ IImageProviderResource,
13
+ IImageResource,
14
+ IImageUploadContextResolved,
15
+ IImageUploadInput,
16
+ IImageUploadOptions,
17
+ IImageUploadUrlInput,
18
+ IImageVariantRequest,
19
+ IImageView,
20
+ TypeImageVariantInput,
21
+ } from '../types/image.ts';
22
+ import type {
23
+ IDecoratorImageProviderOptions,
24
+ IImageProviderClientOptions,
25
+ IImageProviderExecute,
26
+ IImageProviderRecord,
27
+ } from '../types/imageProvider.ts';
28
+ import type { IImageSceneRecord } from '../types/imageScene.ts';
29
+
30
+ import { resolveImageVariantRequest } from '../lib/imageVariant.ts';
31
+
32
+ interface IImageProviderContext {
33
+ beanImageProvider: IImageProviderExecute;
34
+ clientOptions: IImageProviderClientOptions;
35
+ onionOptions: IDecoratorImageProviderOptions;
36
+ }
37
+
38
+ @Bean()
39
+ export class BeanImage extends BeanBase {
40
+ async upload<N extends keyof IImageProviderRecord>(
41
+ providerName: N,
42
+ input: IImageUploadInput,
43
+ options?: IImageUploadOptions,
44
+ ): Promise<IImageResource> {
45
+ const providerContext = await this._getProviderContextByInput(providerName, options);
46
+ const imageProviderResource = await providerContext.beanImageProvider.upload(
47
+ { ...input, meta: options?.meta ?? input.meta },
48
+ providerContext.clientOptions,
49
+ providerContext.onionOptions,
50
+ );
51
+ const image = await this._insertImage(
52
+ providerName,
53
+ providerContext.clientName,
54
+ imageProviderResource,
55
+ {
56
+ imageScene: options?.imageScene,
57
+ },
58
+ );
59
+ return this._combineImageResource(image, imageProviderResource);
60
+ }
61
+
62
+ async uploadUrl<N extends keyof IImageProviderRecord>(
63
+ providerName: N,
64
+ input: IImageUploadUrlInput,
65
+ options?: IImageUploadOptions,
66
+ ): Promise<IImageResource> {
67
+ const providerContext = await this._getProviderContextByInput(providerName, options);
68
+ if (!providerContext.beanImageProvider.uploadUrl) {
69
+ throw new Error(`Image provider does not support uploadUrl: ${String(providerName)}`);
70
+ }
71
+ const imageProviderResource = await providerContext.beanImageProvider.uploadUrl(
72
+ { ...input, meta: options?.meta ?? input.meta },
73
+ providerContext.clientOptions,
74
+ providerContext.onionOptions,
75
+ );
76
+ const image = await this._insertImage(
77
+ providerName,
78
+ providerContext.clientName,
79
+ imageProviderResource,
80
+ {
81
+ imageScene: options?.imageScene,
82
+ },
83
+ );
84
+ return this._combineImageResource(image, imageProviderResource);
85
+ }
86
+
87
+ async createDirectUpload<N extends keyof IImageProviderRecord>(
88
+ providerName: N,
89
+ input: IImageDirectUploadInput,
90
+ options?: IImageUploadOptions,
91
+ ): Promise<IImageDirectUploadResult> {
92
+ const providerContext = await this._getProviderContextByInput(providerName, options);
93
+ if (!providerContext.beanImageProvider.createDirectUpload) {
94
+ throw new Error(
95
+ `Image provider does not support createDirectUpload: ${String(providerName)}`,
96
+ );
97
+ }
98
+ const imageProviderResource = await providerContext.beanImageProvider.createDirectUpload(
99
+ { ...input, meta: options?.meta ?? input.meta },
100
+ providerContext.clientOptions,
101
+ providerContext.onionOptions,
102
+ );
103
+ const image = await this._insertImage(
104
+ providerName,
105
+ providerContext.clientName,
106
+ imageProviderResource,
107
+ {
108
+ imageScene: options?.imageScene,
109
+ },
110
+ );
111
+ return {
112
+ ...this._combineImageResource(image, imageProviderResource),
113
+ uploadUrl: imageProviderResource.uploadUrl,
114
+ draft: imageProviderResource.draft,
115
+ };
116
+ }
117
+
118
+ async get(imageId: TableIdentity): Promise<IImageResource | undefined> {
119
+ const image = await this.scope.model.image.getById(imageId);
120
+ if (!image) return;
121
+ const imageProviderResource = await this._getImageProviderResource(image);
122
+ return this._combineImageResource(image, imageProviderResource);
123
+ }
124
+
125
+ async delete(imageId: TableIdentity) {
126
+ const image = await this.scope.model.image.getById(imageId);
127
+ if (!image) return;
128
+ const { beanImageProvider, clientOptions, onionOptions } =
129
+ await this._getProviderContext(image);
130
+ await beanImageProvider.delete(image, clientOptions, onionOptions);
131
+ await this.scope.model.image.deleteById(image.id);
132
+ }
133
+
134
+ async getVariantUrl(
135
+ imageId: TableIdentity,
136
+ request?: TypeImageVariantInput,
137
+ deliveryOptions?: IImageDeliveryOptions,
138
+ ) {
139
+ const image = await this.scope.model.image.getById(imageId);
140
+ if (!image) throw new Error(`not found image: ${imageId}`);
141
+ const requestNormalized = this._normalizeVariantRequest(request);
142
+ const deliveryOptionsResolved = this._mergeDeliveryOptions(
143
+ image,
144
+ requestNormalized,
145
+ deliveryOptions,
146
+ );
147
+ const { beanImageProvider, clientOptions, onionOptions } =
148
+ await this._getProviderContext(image);
149
+ if (
150
+ deliveryOptionsResolved.signed &&
151
+ (clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy'
152
+ ) {
153
+ return await this._createSignedDeliveryUrl(
154
+ image.id,
155
+ requestNormalized,
156
+ deliveryOptionsResolved,
157
+ );
158
+ }
159
+ return await beanImageProvider.getVariantUrl(
160
+ image,
161
+ requestNormalized,
162
+ clientOptions,
163
+ onionOptions,
164
+ deliveryOptionsResolved,
165
+ );
166
+ }
167
+
168
+ async download(
169
+ imageId: TableIdentity,
170
+ request?: TypeImageVariantInput,
171
+ deliveryOptions?: IImageDeliveryOptions,
172
+ ) {
173
+ const image = await this.scope.model.image.getById(imageId);
174
+ if (!image) throw new Error(`not found image: ${imageId}`);
175
+ const requestNormalized = this._normalizeVariantRequest(request);
176
+ const deliveryOptionsResolved = this._mergeDeliveryOptions(
177
+ image,
178
+ requestNormalized,
179
+ deliveryOptions,
180
+ );
181
+ const { beanImageProvider, clientOptions, onionOptions } =
182
+ await this._getProviderContext(image);
183
+ if (
184
+ deliveryOptionsResolved.signed &&
185
+ (clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy'
186
+ ) {
187
+ return {
188
+ kind: 'url' as const,
189
+ url: await this._createSignedDeliveryUrl(
190
+ image.id,
191
+ requestNormalized,
192
+ deliveryOptionsResolved,
193
+ ),
194
+ filename: image.filename,
195
+ contentType: image.contentType,
196
+ signed: true,
197
+ };
198
+ }
199
+ if (beanImageProvider.download) {
200
+ return await beanImageProvider.download(
201
+ image,
202
+ requestNormalized,
203
+ clientOptions,
204
+ onionOptions,
205
+ deliveryOptionsResolved,
206
+ );
207
+ }
208
+ return {
209
+ kind: 'url' as const,
210
+ url: await beanImageProvider.getVariantUrl(
211
+ image,
212
+ requestNormalized,
213
+ clientOptions,
214
+ onionOptions,
215
+ deliveryOptionsResolved,
216
+ ),
217
+ filename: image.filename,
218
+ contentType: image.contentType,
219
+ signed: !!deliveryOptionsResolved.signed,
220
+ };
221
+ }
222
+
223
+ async resolveView(
224
+ imageId?: TableIdentity,
225
+ request?: TypeImageVariantInput,
226
+ imageScene?: keyof IImageSceneRecord,
227
+ deliveryOptions?: IImageDeliveryOptions,
228
+ ): Promise<IImageView | undefined> {
229
+ if (!imageId) return;
230
+ const image = await this.get(imageId);
231
+ if (!image) return;
232
+ if (imageScene && image.imageScene !== imageScene) {
233
+ throw new Error(`image scene mismatch: image=${image.imageScene}, expected=${imageScene}`);
234
+ }
235
+ const requestNormalized = this._normalizeVariantRequest(request);
236
+ const deliveryOptionsResolved = this._mergeDeliveryOptions(
237
+ image,
238
+ requestNormalized,
239
+ deliveryOptions,
240
+ );
241
+ return {
242
+ id: image.id,
243
+ url: await this.getVariantUrl(image.id, requestNormalized, deliveryOptionsResolved),
244
+ filename: image.filename,
245
+ width: image.width,
246
+ height: image.height,
247
+ provider: image.provider,
248
+ clientName: image.clientName,
249
+ imageScene: image.imageScene,
250
+ uploadedAt: image.uploadedAt,
251
+ variants: image.variants,
252
+ signed: !!deliveryOptionsResolved.signed,
253
+ };
254
+ }
255
+
256
+ async resolveViews(
257
+ imageIds?: TableIdentity[],
258
+ request?: TypeImageVariantInput,
259
+ imageScene?: keyof IImageSceneRecord,
260
+ deliveryOptions?: IImageDeliveryOptions,
261
+ ) {
262
+ if (!imageIds) return;
263
+ if (!imageIds.length) return [];
264
+ const items = await Promise.all(
265
+ imageIds.map(imageId => this.resolveView(imageId, request, imageScene, deliveryOptions)),
266
+ );
267
+ return items.filter((item): item is IImageView => !!item);
268
+ }
269
+
270
+ private async _getImageProviderResource(image: EntityImage) {
271
+ const { beanImageProvider, clientOptions, onionOptions } =
272
+ await this._getProviderContext(image);
273
+ return await beanImageProvider.get(image, clientOptions, onionOptions);
274
+ }
275
+
276
+ private async _getProviderContext(image: EntityImage): Promise<IImageProviderContext> {
277
+ const providerContext = await this.bean.imageProvider.getClientOptions({
278
+ providerName: image.providerName,
279
+ clientName: image.clientName,
280
+ });
281
+ if (!providerContext.entityImageProvider) {
282
+ throw new Error(
283
+ `Image provider not found: ${String(image.providerName)}.${image.clientName}`,
284
+ );
285
+ }
286
+ return {
287
+ beanImageProvider: this._getBeanImageProvider(
288
+ providerContext.beanFullName as keyof IImageProviderRecord,
289
+ ),
290
+ clientOptions: this._normalizeClientOptions(providerContext.clientOptions),
291
+ onionOptions: this._normalizeOnionOptions(providerContext.onionOptions),
292
+ };
293
+ }
294
+
295
+ private async _getProviderContextByInput<N extends keyof IImageProviderRecord>(
296
+ providerName: N,
297
+ options?: IImageUploadOptions,
298
+ ) {
299
+ const clientName = options?.clientName ?? 'default';
300
+ const providerContext = await this.bean.imageProvider.getClientOptions(
301
+ {
302
+ providerName,
303
+ clientName,
304
+ },
305
+ options?.clientOptions,
306
+ );
307
+ if (!providerContext.entityImageProvider || providerContext.disabled) {
308
+ return this.app.throw(403);
309
+ }
310
+ return {
311
+ clientName,
312
+ beanImageProvider: this._getBeanImageProvider(
313
+ providerContext.beanFullName as keyof IImageProviderRecord,
314
+ ),
315
+ clientOptions: this._normalizeClientOptions(providerContext.clientOptions),
316
+ onionOptions: this._normalizeOnionOptions(providerContext.onionOptions),
317
+ };
318
+ }
319
+
320
+ private _getBeanImageProvider(beanFullName: keyof IImageProviderRecord): IImageProviderExecute {
321
+ return this.app.bean._getBean<IImageProviderExecute>(beanFullName as never);
322
+ }
323
+
324
+ private _normalizeClientOptions(
325
+ clientOptions: IImageProviderClientOptions | undefined,
326
+ ): IImageProviderClientOptions {
327
+ return clientOptions ?? {};
328
+ }
329
+
330
+ private _normalizeOnionOptions(
331
+ onionOptions: IDecoratorImageProviderOptions | undefined,
332
+ ): IDecoratorImageProviderOptions {
333
+ return onionOptions ?? {};
334
+ }
335
+
336
+ private _normalizeVariantRequest(request?: TypeImageVariantInput): IImageVariantRequest {
337
+ if (typeof request === 'string') {
338
+ return { variantName: request };
339
+ }
340
+ const defaultVariant = this.scope.config.image.defaultVariant as Parameters<
341
+ typeof resolveImageVariantRequest
342
+ >[1];
343
+ return resolveImageVariantRequest(request ?? {}, defaultVariant);
344
+ }
345
+
346
+ private _mergeDeliveryOptions(
347
+ image: Pick<IImageResource, 'requireSignedURLs'>,
348
+ request: IImageVariantRequest,
349
+ deliveryOptions?: IImageDeliveryOptions,
350
+ ): IImageDeliveryOptions {
351
+ const signed = deliveryOptions?.signed ?? request.signed ?? image.requireSignedURLs ?? false;
352
+ const expiresIn = deliveryOptions?.expiresIn ?? request.expiresIn;
353
+ const expiresAt = deliveryOptions?.expiresAt ?? request.expiresAt;
354
+ const responseMode = deliveryOptions?.responseMode ?? request.responseMode;
355
+ return {
356
+ signed,
357
+ expiresIn,
358
+ expiresAt,
359
+ responseMode,
360
+ };
361
+ }
362
+
363
+ private async _createSignedDeliveryUrl(
364
+ imageId: TableIdentity,
365
+ request: IImageVariantRequest,
366
+ deliveryOptions: IImageDeliveryOptions,
367
+ ) {
368
+ const image = await this.scope.model.image.getById(imageId);
369
+ if (!image) throw new Error(`not found image: ${imageId}`);
370
+ const { beanImageProvider, clientOptions, onionOptions } =
371
+ await this._getProviderContext(image);
372
+ const targetUrl = await beanImageProvider.getVariantUrl(
373
+ image,
374
+ request,
375
+ clientOptions,
376
+ onionOptions,
377
+ {
378
+ signed: false,
379
+ responseMode: 'url',
380
+ },
381
+ );
382
+ const routePath = this.scope.util.combineApiPath(`image/delivery/${imageId}`, false, true);
383
+ const tokenPayload = await this.bean.imageUploadPolicy.createDeliveryToken({
384
+ imageId,
385
+ request,
386
+ targetUrl,
387
+ expiresIn: deliveryOptions.expiresIn,
388
+ });
389
+ const routeUrl = this.app.util.getAbsoluteUrlByApiPath(routePath);
390
+ const url = new URL(routeUrl);
391
+ url.searchParams.set('token', tokenPayload.token);
392
+ return url.toString();
393
+ }
394
+
395
+ private async _insertImage(
396
+ providerName: keyof IImageProviderRecord,
397
+ clientName: string,
398
+ imageProviderResource: IImageProviderResource | IImageProviderDirectUploadResource,
399
+ context?: Partial<IImageUploadContextResolved>,
400
+ ) {
401
+ return await this.scope.model.image.insert({
402
+ providerName,
403
+ clientName,
404
+ resourceId: imageProviderResource.resourceId,
405
+ filename: imageProviderResource.filename,
406
+ contentType: imageProviderResource.contentType,
407
+ size: imageProviderResource.size,
408
+ width: imageProviderResource.width,
409
+ height: imageProviderResource.height,
410
+ requireSignedURLs: imageProviderResource.requireSignedURLs,
411
+ variants: imageProviderResource.variants,
412
+ meta: imageProviderResource.meta,
413
+ storagePath: imageProviderResource.storagePath,
414
+ deliveryBaseUrl: imageProviderResource.deliveryBaseUrl,
415
+ imageScene: context?.imageScene,
416
+ });
417
+ }
418
+
419
+ private _combineImageResource(
420
+ image: EntityImage,
421
+ imageProviderResource?: IImageProviderResource,
422
+ ): IImageResource {
423
+ return {
424
+ id: image.id,
425
+ provider: image.providerName,
426
+ clientName: image.clientName,
427
+ imageScene: image.imageScene,
428
+ resourceId: imageProviderResource?.resourceId ?? image.resourceId,
429
+ filename: imageProviderResource?.filename ?? image.filename,
430
+ contentType: imageProviderResource?.contentType ?? image.contentType,
431
+ size: imageProviderResource?.size ?? image.size,
432
+ width: imageProviderResource?.width ?? image.width,
433
+ height: imageProviderResource?.height ?? image.height,
434
+ requireSignedURLs: imageProviderResource?.requireSignedURLs ?? image.requireSignedURLs,
435
+ variants: imageProviderResource?.variants ?? image.variants,
436
+ meta: imageProviderResource?.meta ?? image.meta,
437
+ storagePath: imageProviderResource?.storagePath ?? image.storagePath,
438
+ deliveryBaseUrl: imageProviderResource?.deliveryBaseUrl ?? image.deliveryBaseUrl,
439
+ raw: imageProviderResource?.raw,
440
+ uploadedAt: image.createdAt,
441
+ };
442
+ }
443
+ }
@@ -0,0 +1,55 @@
1
+ import { BeanBase, deepExtend } from 'vona';
2
+ import { Bean } from 'vona-module-a-bean';
3
+
4
+ import type { EntityImageProvider } from '../entity/imageProvider.ts';
5
+ import type { IImageProviderClientOptions, TypeImageProviderPick } from '../types/imageProvider.ts';
6
+
7
+ @Bean()
8
+ export class BeanImageProvider extends BeanBase {
9
+ async get(data: TypeImageProviderPick) {
10
+ if (!data.id && !data.clientName) data = { ...data, clientName: 'default' };
11
+ const res = await this.scope.model.imageProvider.get(data);
12
+ if (res) return res;
13
+ if (data.id) throw new Error(`not found image provider: ${data.id}`);
14
+ if (!data.providerName) throw new Error('Invalid image provider');
15
+ return await this.scope.redlock.lockIsolate('imageProvider.register', async () => {
16
+ return await this._registerImageProviderLock(data);
17
+ });
18
+ }
19
+
20
+ private async _registerImageProviderLock(data: TypeImageProviderPick) {
21
+ const res = await this.scope.model.imageProvider.get(data, { cache: { force: true } });
22
+ if (res) return res;
23
+ const dataNew: Partial<EntityImageProvider> = {
24
+ disabled: false,
25
+ providerName: data.providerName,
26
+ clientName: data.clientName,
27
+ clientOptions: undefined,
28
+ };
29
+ return await this.scope.model.imageProvider.insert(dataNew);
30
+ }
31
+
32
+ async getClientOptions<T extends IImageProviderClientOptions>(
33
+ data: TypeImageProviderPick,
34
+ clientOptionsCustom?: T,
35
+ clientOptionsDefault?: T,
36
+ ) {
37
+ const entityImageProvider = await this.bean.imageProvider.get(data);
38
+ if (!entityImageProvider) return { entityImageProvider: undefined };
39
+ const disabled = entityImageProvider.disabled;
40
+ const imageProviderName = entityImageProvider.providerName;
41
+ const clientName = entityImageProvider.clientName ?? 'default';
42
+ const onionSlice = this.bean.onion.imageProvider.getOnionSliceEnabled(true, imageProviderName);
43
+ if (!onionSlice) throw new Error(`Image provider not found: ${imageProviderName}`);
44
+ const beanFullName = onionSlice.beanOptions.beanFullName;
45
+ const onionOptions = onionSlice.beanOptions.options ?? {};
46
+ const clientOptions: IImageProviderClientOptions = deepExtend(
47
+ clientOptionsDefault ?? {},
48
+ onionOptions?.base,
49
+ onionOptions?.clients?.[clientName],
50
+ entityImageProvider.clientOptions,
51
+ clientOptionsCustom,
52
+ );
53
+ return { entityImageProvider, disabled, beanFullName, onionOptions, clientOptions };
54
+ }
55
+ }
@@ -0,0 +1,174 @@
1
+ import { BeanBase } from 'vona';
2
+ import { Bean } from 'vona-module-a-bean';
3
+
4
+ import type {
5
+ IImageDeliveryTokenPayload,
6
+ IImageUploadContextResolved,
7
+ IImageUploadPolicyResolved,
8
+ IImageUploadTokenPayload,
9
+ } from '../types/image.ts';
10
+ import type {
11
+ IDecoratorImageSceneOptions,
12
+ IDecoratorImageSceneOptionsProvider,
13
+ IImageSceneRecord,
14
+ } from '../types/imageScene.ts';
15
+
16
+ @Bean()
17
+ export class BeanImageUploadPolicy extends BeanBase {
18
+ async createUploadToken(data: {
19
+ imageScene: keyof IImageSceneRecord;
20
+ size: number;
21
+ mimeType: string;
22
+ expiresIn?: number;
23
+ }) {
24
+ const payload = await this.resolveUploadPolicy(data);
25
+ const path = this.scope.util.combineApiPath('image/upload', false, true);
26
+ const token = await this.bean.jwt.createTempAuthToken(
27
+ {
28
+ kind: 'imageUpload',
29
+ ...payload,
30
+ } as IImageUploadTokenPayload,
31
+ {
32
+ path,
33
+ expiresIn: data.expiresIn,
34
+ },
35
+ );
36
+ return { token, expiresIn: data.expiresIn };
37
+ }
38
+
39
+ async createDeliveryToken(data: {
40
+ imageId: number | string;
41
+ request: IImageDeliveryTokenPayload['request'];
42
+ targetUrl: string;
43
+ expiresIn?: number;
44
+ }) {
45
+ const path = this.scope.util.combineApiPath(`image/delivery/${data.imageId}`, false, true);
46
+ const token = await this.bean.jwt.createTempAuthToken(
47
+ {
48
+ kind: 'imageDelivery',
49
+ imageId: data.imageId,
50
+ request: data.request,
51
+ targetUrl: data.targetUrl,
52
+ } as IImageDeliveryTokenPayload,
53
+ {
54
+ path,
55
+ expiresIn: data.expiresIn,
56
+ },
57
+ );
58
+ return { token, expiresIn: data.expiresIn };
59
+ }
60
+
61
+ async verifyUploadToken(token: string | undefined, routePathRaw: string) {
62
+ const payload = (await this.bean.jwt.get('access').verify(token, {
63
+ path: routePathRaw,
64
+ })) as IImageUploadTokenPayload | undefined;
65
+ if (!payload || payload.kind !== 'imageUpload') {
66
+ return this.app.throw(401);
67
+ }
68
+ return payload;
69
+ }
70
+
71
+ async verifyDeliveryToken(token: string | undefined, routePathRaw: string) {
72
+ const payload = (await this.bean.jwt.get('access').verify(token, {
73
+ path: routePathRaw,
74
+ })) as IImageDeliveryTokenPayload | undefined;
75
+ if (!payload || payload.kind !== 'imageDelivery') {
76
+ return this.app.throw(401);
77
+ }
78
+ return payload;
79
+ }
80
+
81
+ async resolveUploadContext(data: {
82
+ imageScene: keyof IImageSceneRecord;
83
+ }): Promise<IImageUploadContextResolved> {
84
+ const imageScene = data.imageScene;
85
+ const sceneOptions = this._getSceneOptions(imageScene);
86
+ const { providerName, clientName } = await this._resolveProvider(sceneOptions);
87
+ return {
88
+ imageScene,
89
+ providerName,
90
+ clientName,
91
+ meta: await this._resolveSceneMeta(sceneOptions),
92
+ };
93
+ }
94
+
95
+ async resolveUploadPolicy(data: {
96
+ imageScene: keyof IImageSceneRecord;
97
+ size: number;
98
+ mimeType: string;
99
+ }): Promise<IImageUploadPolicyResolved> {
100
+ const imageConfig = this.scope.config.image;
101
+ const context = await this.resolveUploadContext({ imageScene: data.imageScene });
102
+ const { entityImageProvider, disabled } = await this.bean.imageProvider.getClientOptions({
103
+ providerName: context.providerName,
104
+ clientName: context.clientName,
105
+ });
106
+ if (!entityImageProvider || disabled) {
107
+ return this.app.throw(
108
+ 403,
109
+ `Image provider unavailable: ${context.providerName}.${context.clientName}`,
110
+ );
111
+ }
112
+ const sceneOptions = this._getSceneOptions(context.imageScene);
113
+ const uploadOptions = {
114
+ ...(imageConfig.upload ?? {}),
115
+ ...(sceneOptions.upload ?? {}),
116
+ };
117
+ const maxSize = uploadOptions.maxSize;
118
+ const mimeTypes = [...(uploadOptions.mimeTypes ?? [])];
119
+ const extensions = [...(uploadOptions.extensions ?? [])];
120
+ const mimeType = data.mimeType.toLowerCase();
121
+ if (maxSize && data.size > maxSize) {
122
+ return this.app.throw(403, `image too large: maxSize=${maxSize}`);
123
+ }
124
+ if (mimeTypes.length > 0 && !this._matchesMimeType(mimeType, mimeTypes)) {
125
+ return this.app.throw(403, `unsupported image mimeType: ${mimeType}`);
126
+ }
127
+ return {
128
+ ...context,
129
+ maxSize,
130
+ mimeTypes: mimeTypes.length > 0 ? mimeTypes : undefined,
131
+ extensions: extensions.length > 0 ? extensions : undefined,
132
+ multiple: uploadOptions.multiple,
133
+ fileSize: data.size,
134
+ mimeType,
135
+ };
136
+ }
137
+
138
+ private _getSceneOptions(imageScene: keyof IImageSceneRecord): IDecoratorImageSceneOptions {
139
+ const onionSlice = this.bean.onion.imageScene.getOnionSlice(imageScene);
140
+ if (!onionSlice) throw new Error(`not found image scene: ${imageScene}`);
141
+ return onionSlice.beanOptions.options ?? {};
142
+ }
143
+
144
+ private async _resolveProvider(
145
+ sceneOptions: IDecoratorImageSceneOptions,
146
+ ): Promise<Required<IDecoratorImageSceneOptionsProvider>> {
147
+ const provider =
148
+ typeof sceneOptions.provider === 'function'
149
+ ? await sceneOptions.provider(this.ctx)
150
+ : sceneOptions.provider;
151
+ const providerName = provider?.providerName ?? this.scope.config.image.defaultProvider;
152
+ const clientName = provider?.clientName ?? this.scope.config.image.defaultClientName;
153
+ return { providerName, clientName };
154
+ }
155
+
156
+ private async _resolveSceneMeta(sceneOptions: IDecoratorImageSceneOptions) {
157
+ const meta = sceneOptions.meta;
158
+ if (!meta) return undefined;
159
+ if (typeof meta === 'function') {
160
+ return await meta(this.ctx);
161
+ }
162
+ return meta;
163
+ }
164
+
165
+ private _matchesMimeType(mimeType: string, mimeTypes: string[]) {
166
+ return mimeTypes.some(item => {
167
+ if (item === mimeType) return true;
168
+ if (item.endsWith('/*')) {
169
+ return mimeType.startsWith(`${item.slice(0, -1)}`);
170
+ }
171
+ return false;
172
+ });
173
+ }
174
+ }
@@ -0,0 +1,11 @@
1
+ import { Meta } from 'vona-module-a-meta';
2
+ import { BeanRedlockBase } from 'vona-module-a-redlock';
3
+
4
+ export type TypeRedlockLockResource = never;
5
+ export type TypeRedlockLockIsolateResource = 'imageProvider.register';
6
+
7
+ @Meta()
8
+ export class MetaRedlock extends BeanRedlockBase<
9
+ TypeRedlockLockResource,
10
+ TypeRedlockLockIsolateResource
11
+ > {}