cabloy 5.1.97 → 5.1.98

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 (122) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/cabloy-docs/.vitepress/config.mjs +7 -0
  4. package/cabloy-docs/backend/image-guide.md +544 -0
  5. package/cabloy-docs/frontend/image-guide.md +512 -0
  6. package/cabloy-docs/frontend/introduction.md +2 -0
  7. package/cabloy-docs/frontend/ioc-and-beans.md +34 -3
  8. package/cabloy-docs/frontend/model-architecture.md +2 -0
  9. package/cabloy-docs/frontend/model-resource-owner-pattern.md +2 -0
  10. package/cabloy-docs/frontend/model-state-guide.md +2 -0
  11. package/cabloy-docs/frontend/reading-zova-for-vue-developers.md +10 -8
  12. package/cabloy-docs/frontend/server-data.md +2 -0
  13. package/cabloy-docs/frontend/state-architecture-for-vue-developers.md +218 -0
  14. package/cabloy-docs/frontend/system-startup-guide.md +2 -0
  15. package/cabloy-docs/frontend/zova-vs-vue3-comparison.md +11 -10
  16. package/cabloy-docs/fullstack/image-workflow.md +403 -0
  17. package/cabloy-docs/fullstack/introduction.md +1 -0
  18. package/package.json +1 -1
  19. package/vona/package.original.json +0 -1
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/pnpm-lock.yaml +424 -47
  22. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +4 -0
  23. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +4 -0
  24. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +3 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssrHandler.ts +11 -2
  27. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +6 -0
  28. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  29. package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileProvider/boilerplate/{{sceneName}}.{{beanName}}.ts_ +23 -0
  30. package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileScene/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -0
  31. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +66 -0
  32. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +425 -0
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/locales.ts +18 -0
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/this.ts +2 -0
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +322 -0
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileProvider.ts +62 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +196 -0
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +11 -0
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +38 -0
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +16 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/en-us.ts +1 -0
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/zh-cn.ts +1 -0
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +167 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +33 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +41 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +12 -0
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +59 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenRequest.ts +24 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenResponse.ts +16 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadUrlRequest.ts +33 -0
  51. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +55 -0
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/fileProvider.ts +24 -0
  53. package/vona/src/suite-vendor/a-file/modules/a-file/src/index.ts +4 -0
  54. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileProvider.ts +7 -0
  55. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileScene.ts +7 -0
  56. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadValidation.ts +15 -0
  57. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +3 -0
  58. package/vona/src/suite-vendor/a-file/modules/a-file/src/model/file.ts +10 -0
  59. package/vona/src/suite-vendor/a-file/modules/a-file/src/model/fileProvider.ts +10 -0
  60. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +144 -0
  61. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +111 -0
  62. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +53 -0
  63. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/index.ts +3 -0
  64. package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.build.json +11 -0
  65. package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.json +7 -0
  66. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +53 -0
  67. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/index.ts +104 -0
  68. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/this.ts +2 -0
  69. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +123 -0
  70. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/index.ts +1 -0
  71. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +245 -0
  72. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.build.json +11 -0
  73. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.json +7 -0
  74. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +50 -0
  75. package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/index.ts +111 -0
  76. package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/this.ts +2 -0
  77. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +125 -0
  78. package/vona/src/suite-vendor/a-file/modules/file-native/src/config/config.ts +7 -0
  79. package/vona/src/suite-vendor/a-file/modules/file-native/src/index.ts +1 -0
  80. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +125 -0
  81. package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.build.json +11 -0
  82. package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.json +7 -0
  83. package/vona/src/suite-vendor/a-file/package.json +14 -0
  84. package/vona/src/suite-vendor/a-file/tsconfig.base.json +4 -0
  85. package/vona/src/suite-vendor/a-file/tsconfig.json +16 -0
  86. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +4 -1
  87. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +61 -3
  88. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +336 -152
  89. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageProvider.ts +21 -14
  90. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +59 -11
  91. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.index.ts +12 -0
  92. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.redlock.ts +1 -1
  93. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +3 -0
  94. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/schedule.imageDraftPrune.ts +29 -0
  95. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +3 -0
  96. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +24 -54
  97. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeRequest.ts +13 -0
  98. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeResponse.ts +10 -0
  99. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +10 -0
  100. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +10 -1
  101. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +10 -1
  102. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +10 -1
  103. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageUploadValidation.ts +15 -0
  104. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/index.ts +1 -0
  105. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +18 -0
  106. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +28 -2
  107. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  108. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +13 -1
  109. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +26 -0
  110. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +6 -2
  111. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +37 -0
  112. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +21 -1
  113. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +57 -0
  114. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +142 -34
  115. package/vona/src/suite-vendor/a-image/package.json +1 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  117. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/entity.ts +2 -4
  118. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/package.json +1 -1
  119. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/src/lib/columns.ts +7 -3
  120. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  121. package/zova/pnpm-lock.yaml +389 -397
  122. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +0 -9
@@ -8,10 +8,10 @@ import type {
8
8
  IImageDeliveryOptions,
9
9
  IImageDirectUploadInput,
10
10
  IImageDirectUploadResult,
11
+ IImageFinalizeDirectUploadResult,
11
12
  IImageProviderDirectUploadResource,
12
13
  IImageProviderResource,
13
14
  IImageResource,
14
- IImageUploadContextResolved,
15
15
  IImageUploadInput,
16
16
  IImageUploadOptions,
17
17
  IImageUploadUrlInput,
@@ -20,19 +20,34 @@ import type {
20
20
  TypeImageVariantInput,
21
21
  } from '../types/image.ts';
22
22
  import type {
23
- IDecoratorImageProviderOptions,
24
- IImageProviderClientOptions,
25
- IImageProviderExecute,
26
23
  IImageProviderRecord,
24
+ TypeImageProviderClientOptionsByName,
25
+ TypeImageProviderExecuteByName,
26
+ TypeImageProviderOptionsByName,
27
27
  } from '../types/imageProvider.ts';
28
28
  import type { IImageSceneRecord } from '../types/imageScene.ts';
29
29
 
30
30
  import { resolveImageVariantRequest } from '../lib/imageVariant.ts';
31
31
 
32
- interface IImageProviderContext {
33
- beanImageProvider: IImageProviderExecute;
34
- clientOptions: IImageProviderClientOptions;
35
- onionOptions: IDecoratorImageProviderOptions;
32
+ interface IImageProviderContext<N extends keyof IImageProviderRecord = keyof IImageProviderRecord> {
33
+ beanImageProvider: TypeImageProviderExecuteByName<N>;
34
+ clientOptions: TypeImageProviderClientOptionsByName<N>;
35
+ onionOptions: TypeImageProviderOptionsByName<N>;
36
+ }
37
+
38
+ interface IImageDeliveryContext {
39
+ image: EntityImage | IImageResource;
40
+ providerImage: EntityImage;
41
+ requestNormalized: IImageVariantRequest;
42
+ deliveryOptionsResolved: IImageDeliveryOptions;
43
+ providerContext: IImageProviderContext;
44
+ }
45
+
46
+ interface IInsertImageContext {
47
+ imageScene?: keyof IImageSceneRecord;
48
+ status?: EntityImage['status'];
49
+ draftExpiresAt?: Date;
50
+ finalizedAt?: Date;
36
51
  }
37
52
 
38
53
  @Bean()
@@ -40,7 +55,7 @@ export class BeanImage extends BeanBase {
40
55
  async upload<N extends keyof IImageProviderRecord>(
41
56
  providerName: N,
42
57
  input: IImageUploadInput,
43
- options?: IImageUploadOptions,
58
+ options?: IImageUploadOptions<TypeImageProviderClientOptionsByName<N>>,
44
59
  ): Promise<IImageResource> {
45
60
  const providerContext = await this._getProviderContextByInput(providerName, options);
46
61
  const imageProviderResource = await providerContext.beanImageProvider.upload(
@@ -54,6 +69,7 @@ export class BeanImage extends BeanBase {
54
69
  imageProviderResource,
55
70
  {
56
71
  imageScene: options?.imageScene,
72
+ status: 'ready',
57
73
  },
58
74
  );
59
75
  return this._combineImageResource(image, imageProviderResource);
@@ -62,7 +78,7 @@ export class BeanImage extends BeanBase {
62
78
  async uploadUrl<N extends keyof IImageProviderRecord>(
63
79
  providerName: N,
64
80
  input: IImageUploadUrlInput,
65
- options?: IImageUploadOptions,
81
+ options?: IImageUploadOptions<TypeImageProviderClientOptionsByName<N>>,
66
82
  ): Promise<IImageResource> {
67
83
  const providerContext = await this._getProviderContextByInput(providerName, options);
68
84
  if (!providerContext.beanImageProvider.uploadUrl) {
@@ -79,6 +95,7 @@ export class BeanImage extends BeanBase {
79
95
  imageProviderResource,
80
96
  {
81
97
  imageScene: options?.imageScene,
98
+ status: 'ready',
82
99
  },
83
100
  );
84
101
  return this._combineImageResource(image, imageProviderResource);
@@ -87,7 +104,7 @@ export class BeanImage extends BeanBase {
87
104
  async createDirectUpload<N extends keyof IImageProviderRecord>(
88
105
  providerName: N,
89
106
  input: IImageDirectUploadInput,
90
- options?: IImageUploadOptions,
107
+ options?: IImageUploadOptions<TypeImageProviderClientOptionsByName<N>>,
91
108
  ): Promise<IImageDirectUploadResult> {
92
109
  const providerContext = await this._getProviderContextByInput(providerName, options);
93
110
  if (!providerContext.beanImageProvider.createDirectUpload) {
@@ -100,21 +117,74 @@ export class BeanImage extends BeanBase {
100
117
  providerContext.clientOptions,
101
118
  providerContext.onionOptions,
102
119
  );
120
+ const isDraft = imageProviderResource.draft ?? true;
121
+ const draftExpiresAt = isDraft
122
+ ? this._resolveDirectUploadDraftExpiresAt(input.expiry)
123
+ : undefined;
103
124
  const image = await this._insertImage(
104
125
  providerName,
105
126
  providerContext.clientName,
106
127
  imageProviderResource,
107
128
  {
108
129
  imageScene: options?.imageScene,
130
+ status: isDraft ? 'draft' : 'ready',
131
+ draftExpiresAt,
109
132
  },
110
133
  );
111
134
  return {
112
135
  ...this._combineImageResource(image, imageProviderResource),
113
136
  uploadUrl: imageProviderResource.uploadUrl,
114
- draft: imageProviderResource.draft,
137
+ draft: imageProviderResource.draft ?? true,
115
138
  };
116
139
  }
117
140
 
141
+ async finalizeDirectUpload(imageId: TableIdentity): Promise<IImageFinalizeDirectUploadResult> {
142
+ return await this.scope.redlock.lock(`image.directUpload.${imageId}`, async () => {
143
+ const image = await this.scope.model.image.getById(imageId);
144
+ if (!image) throw new Error(`not found image: ${imageId}`);
145
+ if (image.status !== 'draft') {
146
+ return this.app.throw(403, `image is not draft: ${imageId}`);
147
+ }
148
+ const draftExpiresAt = this._normalizeDate(image.draftExpiresAt);
149
+ if (draftExpiresAt && draftExpiresAt.getTime() < Date.now()) {
150
+ await this.scope.model.image.updateById(image.id, {
151
+ status: 'expired',
152
+ });
153
+ return this.app.throw(403, `image draft expired: ${imageId}`);
154
+ }
155
+ const providerContext = await this._getProviderContext(image);
156
+ if (!providerContext.beanImageProvider.finalizeDirectUpload) {
157
+ throw new Error(
158
+ `Image provider does not support finalizeDirectUpload: ${String(image.providerName)}`,
159
+ );
160
+ }
161
+ const imageProviderResource = await providerContext.beanImageProvider.finalizeDirectUpload(
162
+ image,
163
+ providerContext.clientOptions,
164
+ providerContext.onionOptions,
165
+ );
166
+ if (!imageProviderResource) {
167
+ return this.app.throw(403, `image direct upload not ready: ${imageId}`);
168
+ }
169
+ const finalizedAt = new Date();
170
+ const imageUpdated = await this.scope.model.image.updateById(
171
+ image.id,
172
+ this._buildImagePersistData(imageProviderResource, {
173
+ status: 'ready',
174
+ draftExpiresAt,
175
+ finalizedAt,
176
+ }),
177
+ );
178
+ return this._combineImageResource(
179
+ {
180
+ ...image,
181
+ ...imageUpdated,
182
+ },
183
+ imageProviderResource,
184
+ );
185
+ });
186
+ }
187
+
118
188
  async get(imageId: TableIdentity): Promise<IImageResource | undefined> {
119
189
  const image = await this.scope.model.image.getById(imageId);
120
190
  if (!image) return;
@@ -122,6 +192,21 @@ export class BeanImage extends BeanBase {
122
192
  return this._combineImageResource(image, imageProviderResource);
123
193
  }
124
194
 
195
+ async expireDraftImage(imageId: TableIdentity) {
196
+ return await this.scope.redlock.lock(`image.directUpload.${imageId}`, async () => {
197
+ const image = await this.scope.model.image.getById(imageId);
198
+ if (!image || image.status !== 'draft') return;
199
+ const draftExpiresAt = this._normalizeDate(image.draftExpiresAt);
200
+ if (!draftExpiresAt || draftExpiresAt.getTime() >= Date.now()) return;
201
+ const { beanImageProvider, clientOptions, onionOptions } =
202
+ await this._getProviderContext(image);
203
+ await beanImageProvider.delete(image, clientOptions, onionOptions);
204
+ await this.scope.model.image.updateById(image.id, {
205
+ status: 'expired',
206
+ });
207
+ });
208
+ }
209
+
125
210
  async delete(imageId: TableIdentity) {
126
211
  const image = await this.scope.model.image.getById(imageId);
127
212
  if (!image) return;
@@ -136,33 +221,8 @@ export class BeanImage extends BeanBase {
136
221
  request?: TypeImageVariantInput,
137
222
  deliveryOptions?: IImageDeliveryOptions,
138
223
  ) {
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
- );
224
+ const context = await this._getDeliveryContext(imageId, request, deliveryOptions);
225
+ return await this._getVariantUrlByContext(context);
166
226
  }
167
227
 
168
228
  async download(
@@ -170,53 +230,37 @@ export class BeanImage extends BeanBase {
170
230
  request?: TypeImageVariantInput,
171
231
  deliveryOptions?: IImageDeliveryOptions,
172
232
  ) {
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
- ) {
233
+ const context = await this._getDeliveryContext(imageId, request, deliveryOptions);
234
+ if (this._shouldUseProxySignedDelivery(context)) {
187
235
  return {
188
236
  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,
237
+ url: await this._createSignedDeliveryUrl(context),
238
+ filename: context.image.filename,
239
+ contentType: context.image.contentType,
196
240
  signed: true,
197
241
  };
198
242
  }
199
- if (beanImageProvider.download) {
200
- return await beanImageProvider.download(
201
- image,
202
- requestNormalized,
203
- clientOptions,
204
- onionOptions,
205
- deliveryOptionsResolved,
243
+ if (context.providerContext.beanImageProvider.download) {
244
+ return await context.providerContext.beanImageProvider.download(
245
+ context.providerImage,
246
+ context.requestNormalized,
247
+ context.providerContext.clientOptions,
248
+ context.providerContext.onionOptions,
249
+ context.deliveryOptionsResolved,
206
250
  );
207
251
  }
208
252
  return {
209
253
  kind: 'url' as const,
210
- url: await beanImageProvider.getVariantUrl(
211
- image,
212
- requestNormalized,
213
- clientOptions,
214
- onionOptions,
215
- deliveryOptionsResolved,
254
+ url: await context.providerContext.beanImageProvider.getVariantUrl(
255
+ context.providerImage,
256
+ context.requestNormalized,
257
+ context.providerContext.clientOptions,
258
+ context.providerContext.onionOptions,
259
+ context.deliveryOptionsResolved,
216
260
  ),
217
- filename: image.filename,
218
- contentType: image.contentType,
219
- signed: !!deliveryOptionsResolved.signed,
261
+ filename: context.image.filename,
262
+ contentType: context.image.contentType,
263
+ signed: !!context.deliveryOptionsResolved.signed,
220
264
  };
221
265
  }
222
266
 
@@ -227,30 +271,9 @@ export class BeanImage extends BeanBase {
227
271
  deliveryOptions?: IImageDeliveryOptions,
228
272
  ): Promise<IImageView | undefined> {
229
273
  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
- };
274
+ const prepared = await this._prepareImageView(imageId, request, imageScene, deliveryOptions);
275
+ if (!prepared) return;
276
+ return await this._createImageView(prepared.image, prepared.context);
254
277
  }
255
278
 
256
279
  async resolveViews(
@@ -273,29 +296,45 @@ export class BeanImage extends BeanBase {
273
296
  return await beanImageProvider.get(image, clientOptions, onionOptions);
274
297
  }
275
298
 
276
- private async _getProviderContext(image: EntityImage): Promise<IImageProviderContext> {
299
+ private async _prepareImageView(
300
+ imageId: TableIdentity,
301
+ request?: TypeImageVariantInput,
302
+ imageScene?: keyof IImageSceneRecord,
303
+ deliveryOptions?: IImageDeliveryOptions,
304
+ ) {
305
+ const image = await this.get(imageId);
306
+ if (!image) return;
307
+ if (imageScene && image.imageScene !== imageScene) {
308
+ throw new Error(`image scene mismatch: image=${image.imageScene}, expected=${imageScene}`);
309
+ }
310
+ const context = await this._createDeliveryContext(image, request, deliveryOptions);
311
+ return { image, context };
312
+ }
313
+
314
+ private async _getProviderContext(
315
+ image:
316
+ | Pick<EntityImage, 'providerName' | 'clientName'>
317
+ | Pick<IImageResource, 'provider' | 'clientName'>,
318
+ ): Promise<IImageProviderContext> {
319
+ const providerName = 'providerName' in image ? image.providerName : image.provider;
277
320
  const providerContext = await this.bean.imageProvider.getClientOptions({
278
- providerName: image.providerName,
321
+ providerName,
279
322
  clientName: image.clientName,
280
323
  });
281
324
  if (!providerContext.entityImageProvider) {
282
- throw new Error(
283
- `Image provider not found: ${String(image.providerName)}.${image.clientName}`,
284
- );
325
+ throw new Error(`Image provider not found: ${String(providerName)}.${image.clientName}`);
285
326
  }
286
327
  return {
287
- beanImageProvider: this._getBeanImageProvider(
288
- providerContext.beanFullName as keyof IImageProviderRecord,
289
- ),
290
- clientOptions: this._normalizeClientOptions(providerContext.clientOptions),
291
- onionOptions: this._normalizeOnionOptions(providerContext.onionOptions),
328
+ beanImageProvider: this._getBeanImageProvider(providerName, providerContext.beanFullName),
329
+ clientOptions: this._normalizeOptions(providerContext.clientOptions),
330
+ onionOptions: this._normalizeOptions(providerContext.onionOptions),
292
331
  };
293
332
  }
294
333
 
295
334
  private async _getProviderContextByInput<N extends keyof IImageProviderRecord>(
296
335
  providerName: N,
297
- options?: IImageUploadOptions,
298
- ) {
336
+ options?: IImageUploadOptions<TypeImageProviderClientOptionsByName<N>>,
337
+ ): Promise<IImageProviderContext<N> & { clientName: string }> {
299
338
  const clientName = options?.clientName ?? 'default';
300
339
  const providerContext = await this.bean.imageProvider.getClientOptions(
301
340
  {
@@ -309,28 +348,131 @@ export class BeanImage extends BeanBase {
309
348
  }
310
349
  return {
311
350
  clientName,
312
- beanImageProvider: this._getBeanImageProvider(
313
- providerContext.beanFullName as keyof IImageProviderRecord,
314
- ),
315
- clientOptions: this._normalizeClientOptions(providerContext.clientOptions),
316
- onionOptions: this._normalizeOnionOptions(providerContext.onionOptions),
351
+ beanImageProvider: this._getBeanImageProvider(providerName, providerContext.beanFullName),
352
+ clientOptions: this._normalizeOptions(providerContext.clientOptions),
353
+ onionOptions: this._normalizeOptions(providerContext.onionOptions),
317
354
  };
318
355
  }
319
356
 
320
- private _getBeanImageProvider(beanFullName: keyof IImageProviderRecord): IImageProviderExecute {
321
- return this.app.bean._getBean<IImageProviderExecute>(beanFullName as never);
357
+ private _getBeanImageProvider<N extends keyof IImageProviderRecord>(
358
+ _providerName: N,
359
+ beanFullName: string,
360
+ ): TypeImageProviderExecuteByName<N> {
361
+ return this.app.bean._getBean<TypeImageProviderExecuteByName<N>>(beanFullName as never);
322
362
  }
323
363
 
324
- private _normalizeClientOptions(
325
- clientOptions: IImageProviderClientOptions | undefined,
326
- ): IImageProviderClientOptions {
327
- return clientOptions ?? {};
364
+ private _normalizeOptions<T extends object>(options: T | undefined): T {
365
+ return (options ?? {}) as T;
328
366
  }
329
367
 
330
- private _normalizeOnionOptions(
331
- onionOptions: IDecoratorImageProviderOptions | undefined,
332
- ): IDecoratorImageProviderOptions {
333
- return onionOptions ?? {};
368
+ async createImageActionResponse(
369
+ image: IImageResource,
370
+ request?: TypeImageVariantInput,
371
+ deliveryOptions?: IImageDeliveryOptions,
372
+ ) {
373
+ const context = await this._createDeliveryContext(image, request, deliveryOptions);
374
+ return {
375
+ ...image,
376
+ url: await this._getVariantUrlByContext(context),
377
+ signed: !!context.deliveryOptionsResolved.signed,
378
+ };
379
+ }
380
+
381
+ private _resolveDirectUploadDraftExpiresAt(expiry?: IImageDirectUploadInput['expiry']) {
382
+ return (
383
+ this._normalizeDate(expiry) ??
384
+ new Date(Date.now() + this.scope.config.image.directUpload.draftExpiresIn)
385
+ );
386
+ }
387
+
388
+ private _normalizeDate(value: unknown) {
389
+ if (!value) return undefined;
390
+ if (value instanceof Date) {
391
+ return Number.isNaN(value.getTime()) ? undefined : value;
392
+ }
393
+ const date = new Date(value as string | number);
394
+ return Number.isNaN(date.getTime()) ? undefined : date;
395
+ }
396
+
397
+ private async _getDeliveryContext(
398
+ imageId: TableIdentity,
399
+ request?: TypeImageVariantInput,
400
+ deliveryOptions?: IImageDeliveryOptions,
401
+ ): Promise<IImageDeliveryContext> {
402
+ const image = await this.scope.model.image.getById(imageId);
403
+ if (!image) throw new Error(`not found image: ${imageId}`);
404
+ return await this._createDeliveryContext(image, request, deliveryOptions);
405
+ }
406
+
407
+ private async _createDeliveryContext(
408
+ image: EntityImage | IImageResource,
409
+ request?: TypeImageVariantInput,
410
+ deliveryOptions?: IImageDeliveryOptions,
411
+ ): Promise<IImageDeliveryContext> {
412
+ this._assertImageReady(image);
413
+ const providerImage = this._getProviderImage(image);
414
+ const requestNormalized = this._normalizeVariantRequest(request);
415
+ const deliveryOptionsResolved = this._mergeDeliveryOptions(
416
+ image,
417
+ requestNormalized,
418
+ deliveryOptions,
419
+ );
420
+ const providerContext = await this._getProviderContext(image);
421
+ return {
422
+ image,
423
+ providerImage,
424
+ requestNormalized,
425
+ deliveryOptionsResolved,
426
+ providerContext,
427
+ };
428
+ }
429
+
430
+ private async _getVariantUrlByContext(context: IImageDeliveryContext) {
431
+ if (this._shouldUseProxySignedDelivery(context)) {
432
+ return await this._createSignedDeliveryUrl(context);
433
+ }
434
+ return await context.providerContext.beanImageProvider.getVariantUrl(
435
+ context.providerImage,
436
+ context.requestNormalized,
437
+ context.providerContext.clientOptions,
438
+ context.providerContext.onionOptions,
439
+ context.deliveryOptionsResolved,
440
+ );
441
+ }
442
+
443
+ private async _createImageView(image: IImageResource, context: IImageDeliveryContext) {
444
+ return {
445
+ id: image.id,
446
+ url: await this._getVariantUrlByContext(context),
447
+ filename: image.filename,
448
+ width: image.width,
449
+ height: image.height,
450
+ provider: image.provider,
451
+ clientName: image.clientName,
452
+ imageScene: image.imageScene,
453
+ status: image.status,
454
+ draftExpiresAt: image.draftExpiresAt,
455
+ finalizedAt: image.finalizedAt,
456
+ uploadedAt: image.uploadedAt,
457
+ variants: image.variants,
458
+ signed: !!context.deliveryOptionsResolved.signed,
459
+ } satisfies IImageView;
460
+ }
461
+
462
+ private _shouldUseProxySignedDelivery(context: IImageDeliveryContext) {
463
+ return (
464
+ context.deliveryOptionsResolved.signed &&
465
+ (context.providerContext.clientOptions.signedDeliveryKind ?? 'proxy') === 'proxy'
466
+ );
467
+ }
468
+
469
+ private _assertImageReady(image: Pick<EntityImage, 'id' | 'status'>) {
470
+ if (image.status === 'draft') {
471
+ return this.app.throw(403, `image draft not ready: ${image.id}`);
472
+ }
473
+ if (image.status === 'expired') {
474
+ return this.app.throw(403, `image draft expired: ${image.id}`);
475
+ }
334
476
  }
335
477
 
336
478
  private _normalizeVariantRequest(request?: TypeImageVariantInput): IImageVariantRequest {
@@ -360,31 +502,27 @@ export class BeanImage extends BeanBase {
360
502
  };
361
503
  }
362
504
 
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,
505
+ private async _createSignedDeliveryUrl(context: IImageDeliveryContext) {
506
+ const targetUrl = await context.providerContext.beanImageProvider.getVariantUrl(
507
+ context.providerImage,
508
+ context.requestNormalized,
509
+ context.providerContext.clientOptions,
510
+ context.providerContext.onionOptions,
377
511
  {
378
512
  signed: false,
379
513
  responseMode: 'url',
380
514
  },
381
515
  );
382
- const routePath = this.scope.util.combineApiPath(`image/delivery/${imageId}`, false, true);
516
+ const routePath = this.scope.util.combineApiPath(
517
+ `image/delivery/${context.image.id}`,
518
+ false,
519
+ true,
520
+ );
383
521
  const tokenPayload = await this.bean.imageUploadPolicy.createDeliveryToken({
384
- imageId,
385
- request,
522
+ imageId: context.image.id,
523
+ request: context.requestNormalized,
386
524
  targetUrl,
387
- expiresIn: deliveryOptions.expiresIn,
525
+ expiresIn: context.deliveryOptionsResolved.expiresIn,
388
526
  });
389
527
  const routeUrl = this.app.util.getAbsoluteUrlByApiPath(routePath);
390
528
  const url = new URL(routeUrl);
@@ -392,15 +530,42 @@ export class BeanImage extends BeanBase {
392
530
  return url.toString();
393
531
  }
394
532
 
395
- private async _insertImage(
396
- providerName: keyof IImageProviderRecord,
397
- clientName: string,
533
+ private _getProviderImage(image: EntityImage | IImageResource): EntityImage {
534
+ if ('providerName' in image) return image;
535
+ return {
536
+ id: image.id,
537
+ providerName: image.provider,
538
+ clientName: image.clientName,
539
+ resourceId: image.resourceId,
540
+ filename: image.filename,
541
+ contentType: image.contentType,
542
+ size: image.size,
543
+ width: image.width,
544
+ height: image.height,
545
+ requireSignedURLs: image.requireSignedURLs,
546
+ variants: image.variants,
547
+ meta: image.meta,
548
+ storagePath: image.storagePath,
549
+ deliveryBaseUrl: image.deliveryBaseUrl,
550
+ imageScene:
551
+ typeof image.imageScene === 'string'
552
+ ? (image.imageScene as keyof IImageSceneRecord)
553
+ : undefined,
554
+ status: image.status,
555
+ draftExpiresAt: image.draftExpiresAt,
556
+ finalizedAt: image.finalizedAt,
557
+ createdAt: image.uploadedAt ?? new Date(0),
558
+ updatedAt: image.uploadedAt ?? new Date(0),
559
+ deleted: false,
560
+ iid: 0,
561
+ } satisfies EntityImage;
562
+ }
563
+
564
+ private _buildImagePersistData(
398
565
  imageProviderResource: IImageProviderResource | IImageProviderDirectUploadResource,
399
- context?: Partial<IImageUploadContextResolved>,
566
+ context?: IInsertImageContext,
400
567
  ) {
401
- return await this.scope.model.image.insert({
402
- providerName,
403
- clientName,
568
+ return {
404
569
  resourceId: imageProviderResource.resourceId,
405
570
  filename: imageProviderResource.filename,
406
571
  contentType: imageProviderResource.contentType,
@@ -413,6 +578,22 @@ export class BeanImage extends BeanBase {
413
578
  storagePath: imageProviderResource.storagePath,
414
579
  deliveryBaseUrl: imageProviderResource.deliveryBaseUrl,
415
580
  imageScene: context?.imageScene,
581
+ status: context?.status,
582
+ draftExpiresAt: context?.draftExpiresAt,
583
+ finalizedAt: context?.finalizedAt,
584
+ };
585
+ }
586
+
587
+ private async _insertImage(
588
+ providerName: keyof IImageProviderRecord,
589
+ clientName: string,
590
+ imageProviderResource: IImageProviderResource | IImageProviderDirectUploadResource,
591
+ context?: IInsertImageContext,
592
+ ) {
593
+ return await this.scope.model.image.insert({
594
+ providerName,
595
+ clientName,
596
+ ...this._buildImagePersistData(imageProviderResource, context),
416
597
  });
417
598
  }
418
599
 
@@ -425,6 +606,9 @@ export class BeanImage extends BeanBase {
425
606
  provider: image.providerName,
426
607
  clientName: image.clientName,
427
608
  imageScene: image.imageScene,
609
+ status: image.status,
610
+ draftExpiresAt: image.status === 'draft' ? image.draftExpiresAt : undefined,
611
+ finalizedAt: image.finalizedAt,
428
612
  resourceId: imageProviderResource?.resourceId ?? image.resourceId,
429
613
  filename: imageProviderResource?.filename ?? image.filename,
430
614
  contentType: imageProviderResource?.contentType ?? image.contentType,