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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +27 -0
- package/cabloy-docs/.vitepress/config.mjs +7 -0
- package/cabloy-docs/backend/image-guide.md +544 -0
- package/cabloy-docs/frontend/image-guide.md +512 -0
- package/cabloy-docs/frontend/introduction.md +2 -0
- package/cabloy-docs/frontend/ioc-and-beans.md +34 -3
- package/cabloy-docs/frontend/model-architecture.md +2 -0
- package/cabloy-docs/frontend/model-resource-owner-pattern.md +2 -0
- package/cabloy-docs/frontend/model-state-guide.md +2 -0
- package/cabloy-docs/frontend/reading-zova-for-vue-developers.md +10 -8
- package/cabloy-docs/frontend/server-data.md +2 -0
- package/cabloy-docs/frontend/state-architecture-for-vue-developers.md +218 -0
- package/cabloy-docs/frontend/system-startup-guide.md +2 -0
- package/cabloy-docs/frontend/zova-vs-vue3-comparison.md +11 -10
- package/cabloy-docs/fullstack/image-workflow.md +403 -0
- package/cabloy-docs/fullstack/introduction.md +1 -0
- package/package.json +1 -1
- package/vona/package.original.json +0 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +424 -47
- package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +4 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +4 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +3 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssrHandler.ts +11 -2
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +6 -0
- package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
- package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileProvider/boilerplate/{{sceneName}}.{{beanName}}.ts_ +23 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileScene/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/package.json +66 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +425 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/locales.ts +18 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +322 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileProvider.ts +62 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +196 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +11 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +38 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +16 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +167 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +33 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +41 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +12 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +59 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenRequest.ts +24 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenResponse.ts +16 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadUrlRequest.ts +33 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +55 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/fileProvider.ts +24 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/index.ts +4 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileProvider.ts +7 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileScene.ts +7 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadValidation.ts +15 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/model/file.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/model/fileProvider.ts +10 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +144 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +111 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +53 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/src/types/index.ts +3 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +53 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/index.ts +104 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +123 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +245 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/package.json +50 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/index.ts +111 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +125 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/config/config.ts +7 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +125 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-file/package.json +14 -0
- package/vona/src/suite-vendor/a-file/tsconfig.base.json +4 -0
- package/vona/src/suite-vendor/a-file/tsconfig.json +16 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/package.json +4 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +61 -3
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +336 -152
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageProvider.ts +21 -14
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +59 -11
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.index.ts +12 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.redlock.ts +1 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/schedule.imageDraftPrune.ts +29 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +3 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +24 -54
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeRequest.ts +13 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeResponse.ts +10 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +10 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +10 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +10 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +10 -1
- package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageUploadValidation.ts +15 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +18 -0
- package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +28 -2
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +13 -1
- package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +26 -0
- package/vona/src/suite-vendor/a-image/modules/image-native/package.json +6 -2
- package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +37 -0
- package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +21 -1
- package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +57 -0
- package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +142 -34
- package/vona/src/suite-vendor/a-image/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/entity.ts +2 -4
- package/vona/src/suite-vendor/a-vona/modules/a-ormutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-ormutils/src/lib/columns.ts +7 -3
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/pnpm-lock.yaml +389 -397
- package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +0 -9
|
@@ -2,7 +2,12 @@ import { BeanBase, deepExtend } from 'vona';
|
|
|
2
2
|
import { Bean } from 'vona-module-a-bean';
|
|
3
3
|
|
|
4
4
|
import type { EntityImageProvider } from '../entity/imageProvider.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
IImageProviderRecord,
|
|
7
|
+
TypeImageProviderClientOptionsByName,
|
|
8
|
+
TypeImageProviderOptionsByName,
|
|
9
|
+
TypeImageProviderPick,
|
|
10
|
+
} from '../types/imageProvider.ts';
|
|
6
11
|
|
|
7
12
|
@Bean()
|
|
8
13
|
export class BeanImageProvider extends BeanBase {
|
|
@@ -29,27 +34,29 @@ export class BeanImageProvider extends BeanBase {
|
|
|
29
34
|
return await this.scope.model.imageProvider.insert(dataNew);
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
async getClientOptions<
|
|
33
|
-
data: TypeImageProviderPick,
|
|
34
|
-
clientOptionsCustom?:
|
|
35
|
-
clientOptionsDefault?:
|
|
37
|
+
async getClientOptions<N extends keyof IImageProviderRecord>(
|
|
38
|
+
data: TypeImageProviderPick & { providerName: N },
|
|
39
|
+
clientOptionsCustom?: TypeImageProviderClientOptionsByName<N>,
|
|
40
|
+
clientOptionsDefault?: TypeImageProviderClientOptionsByName<N>,
|
|
36
41
|
) {
|
|
37
42
|
const entityImageProvider = await this.bean.imageProvider.get(data);
|
|
38
43
|
if (!entityImageProvider) return { entityImageProvider: undefined };
|
|
39
44
|
const disabled = entityImageProvider.disabled;
|
|
40
|
-
const imageProviderName = entityImageProvider.providerName;
|
|
45
|
+
const imageProviderName = entityImageProvider.providerName as N;
|
|
41
46
|
const clientName = entityImageProvider.clientName ?? 'default';
|
|
42
47
|
const onionSlice = this.bean.onion.imageProvider.getOnionSliceEnabled(true, imageProviderName);
|
|
43
48
|
if (!onionSlice) throw new Error(`Image provider not found: ${imageProviderName}`);
|
|
44
|
-
const beanFullName = onionSlice.beanOptions.beanFullName;
|
|
45
|
-
const onionOptions =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
const beanFullName = onionSlice.beanOptions.beanFullName as N;
|
|
50
|
+
const onionOptions =
|
|
51
|
+
(onionSlice.beanOptions.options as TypeImageProviderOptionsByName<N> | undefined) ??
|
|
52
|
+
({} as TypeImageProviderOptionsByName<N>);
|
|
53
|
+
const clientOptions = deepExtend(
|
|
54
|
+
(clientOptionsDefault ?? {}) as TypeImageProviderClientOptionsByName<N>,
|
|
55
|
+
onionOptions.base,
|
|
56
|
+
onionOptions.clients?.[clientName as keyof typeof onionOptions.clients],
|
|
57
|
+
entityImageProvider.clientOptions as TypeImageProviderClientOptionsByName<N> | undefined,
|
|
51
58
|
clientOptionsCustom,
|
|
52
|
-
)
|
|
59
|
+
) as TypeImageProviderClientOptionsByName<N>;
|
|
53
60
|
return { entityImageProvider, disabled, beanFullName, onionOptions, clientOptions };
|
|
54
61
|
}
|
|
55
62
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import fse from 'fs-extra';
|
|
1
2
|
import { BeanBase } from 'vona';
|
|
2
3
|
import { Bean } from 'vona-module-a-bean';
|
|
3
4
|
|
|
4
5
|
import type {
|
|
5
6
|
IImageDeliveryTokenPayload,
|
|
7
|
+
IImageDirectUploadTokenPayload,
|
|
6
8
|
IImageUploadContextResolved,
|
|
7
9
|
IImageUploadPolicyResolved,
|
|
8
10
|
IImageUploadTokenPayload,
|
|
@@ -13,6 +15,8 @@ import type {
|
|
|
13
15
|
IImageSceneRecord,
|
|
14
16
|
} from '../types/imageScene.ts';
|
|
15
17
|
|
|
18
|
+
import { getImageExtension, matchesImageMimeType } from '../lib/imageUploadValidation.ts';
|
|
19
|
+
|
|
16
20
|
@Bean()
|
|
17
21
|
export class BeanImageUploadPolicy extends BeanBase {
|
|
18
22
|
async createUploadToken(data: {
|
|
@@ -36,6 +40,21 @@ export class BeanImageUploadPolicy extends BeanBase {
|
|
|
36
40
|
return { token, expiresIn: data.expiresIn };
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
async createDirectUploadToken(data: { resourceId: string; path: string; expiresIn?: number }) {
|
|
44
|
+
const path = data.path;
|
|
45
|
+
const token = await this.bean.jwt.createTempAuthToken(
|
|
46
|
+
{
|
|
47
|
+
kind: 'imageDirectUpload',
|
|
48
|
+
resourceId: data.resourceId,
|
|
49
|
+
} as IImageDirectUploadTokenPayload,
|
|
50
|
+
{
|
|
51
|
+
path,
|
|
52
|
+
expiresIn: data.expiresIn,
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
return { token, expiresIn: data.expiresIn };
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
async createDeliveryToken(data: {
|
|
40
59
|
imageId: number | string;
|
|
41
60
|
request: IImageDeliveryTokenPayload['request'];
|
|
@@ -68,6 +87,16 @@ export class BeanImageUploadPolicy extends BeanBase {
|
|
|
68
87
|
return payload;
|
|
69
88
|
}
|
|
70
89
|
|
|
90
|
+
async verifyDirectUploadToken(token: string | undefined, routePathRaw: string) {
|
|
91
|
+
const payload = (await this.bean.jwt.get('access').verify(token, {
|
|
92
|
+
path: routePathRaw,
|
|
93
|
+
})) as IImageDirectUploadTokenPayload | undefined;
|
|
94
|
+
if (!payload || payload.kind !== 'imageDirectUpload') {
|
|
95
|
+
return this.app.throw(401);
|
|
96
|
+
}
|
|
97
|
+
return payload;
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
async verifyDeliveryToken(token: string | undefined, routePathRaw: string) {
|
|
72
101
|
const payload = (await this.bean.jwt.get('access').verify(token, {
|
|
73
102
|
path: routePathRaw,
|
|
@@ -121,7 +150,7 @@ export class BeanImageUploadPolicy extends BeanBase {
|
|
|
121
150
|
if (maxSize && data.size > maxSize) {
|
|
122
151
|
return this.app.throw(403, `image too large: maxSize=${maxSize}`);
|
|
123
152
|
}
|
|
124
|
-
if (mimeTypes.length > 0 && !
|
|
153
|
+
if (mimeTypes.length > 0 && !matchesImageMimeType(mimeType, mimeTypes)) {
|
|
125
154
|
return this.app.throw(403, `unsupported image mimeType: ${mimeType}`);
|
|
126
155
|
}
|
|
127
156
|
return {
|
|
@@ -135,6 +164,35 @@ export class BeanImageUploadPolicy extends BeanBase {
|
|
|
135
164
|
};
|
|
136
165
|
}
|
|
137
166
|
|
|
167
|
+
async validateUploadFile(
|
|
168
|
+
file: {
|
|
169
|
+
file: string;
|
|
170
|
+
filename?: string;
|
|
171
|
+
mimeType: string;
|
|
172
|
+
},
|
|
173
|
+
payload: IImageUploadPolicyResolved,
|
|
174
|
+
) {
|
|
175
|
+
const stat = await fse.stat(file.file);
|
|
176
|
+
const fileSize = Number(stat.size);
|
|
177
|
+
if (fileSize !== payload.fileSize) {
|
|
178
|
+
return this.app.throw(403, `image size mismatch: size=${fileSize}`);
|
|
179
|
+
}
|
|
180
|
+
if (payload.maxSize && fileSize > payload.maxSize) {
|
|
181
|
+
return this.app.throw(403, `image too large: maxSize=${payload.maxSize}`);
|
|
182
|
+
}
|
|
183
|
+
const mimeType = file.mimeType.toLowerCase();
|
|
184
|
+
if (mimeType !== payload.mimeType) {
|
|
185
|
+
return this.app.throw(403, `image mimeType mismatch: mimeType=${mimeType}`);
|
|
186
|
+
}
|
|
187
|
+
if (payload.mimeTypes?.length && !matchesImageMimeType(mimeType, payload.mimeTypes)) {
|
|
188
|
+
return this.app.throw(403, `unsupported image mimeType: ${mimeType}`);
|
|
189
|
+
}
|
|
190
|
+
const extension = getImageExtension(file.filename);
|
|
191
|
+
if (payload.extensions?.length && extension && !payload.extensions.includes(extension)) {
|
|
192
|
+
return this.app.throw(403, `unsupported image extension: ${extension}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
138
196
|
private _getSceneOptions(imageScene: keyof IImageSceneRecord): IDecoratorImageSceneOptions {
|
|
139
197
|
const onionSlice = this.bean.onion.imageScene.getOnionSlice(imageScene);
|
|
140
198
|
if (!onionSlice) throw new Error(`not found image scene: ${imageScene}`);
|
|
@@ -161,14 +219,4 @@ export class BeanImageUploadPolicy extends BeanBase {
|
|
|
161
219
|
}
|
|
162
220
|
return meta;
|
|
163
221
|
}
|
|
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
222
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IMetaOptionsIndex } from 'vona-module-a-index';
|
|
2
|
+
|
|
3
|
+
import { BeanBase } from 'vona';
|
|
4
|
+
import { Meta } from 'vona-module-a-meta';
|
|
5
|
+
import { $tableColumns } from 'vona-module-a-ormutils';
|
|
6
|
+
|
|
7
|
+
@Meta<IMetaOptionsIndex>({
|
|
8
|
+
indexes: {
|
|
9
|
+
...$tableColumns('aImage', 'status+draftExpiresAt'),
|
|
10
|
+
},
|
|
11
|
+
})
|
|
12
|
+
export class MetaIndex extends BeanBase {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Meta } from 'vona-module-a-meta';
|
|
2
2
|
import { BeanRedlockBase } from 'vona-module-a-redlock';
|
|
3
3
|
|
|
4
|
-
export type TypeRedlockLockResource =
|
|
4
|
+
export type TypeRedlockLockResource = `image.directUpload.${string}`;
|
|
5
5
|
export type TypeRedlockLockIsolateResource = 'imageProvider.register';
|
|
6
6
|
|
|
7
7
|
@Meta()
|
|
@@ -34,6 +34,9 @@ export class MetaVersion extends BeanBase implements IMetaVersionUpdate {
|
|
|
34
34
|
table.text(entityImage.storagePath);
|
|
35
35
|
table.text(entityImage.deliveryBaseUrl);
|
|
36
36
|
table.string(entityImage.imageScene, 255);
|
|
37
|
+
table.string(entityImage.status, 255).defaultTo('ready');
|
|
38
|
+
table.dateTime(entityImage.draftExpiresAt);
|
|
39
|
+
table.dateTime(entityImage.finalizedAt);
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { IScheduleExecute } from 'vona-module-a-schedule';
|
|
2
|
+
|
|
3
|
+
import { BeanBase } from 'vona';
|
|
4
|
+
import { Schedule } from 'vona-module-a-schedule';
|
|
5
|
+
|
|
6
|
+
const PruneBatchLimit = 100;
|
|
7
|
+
|
|
8
|
+
@Schedule({ repeat: { every: 30 * 60 * 1000 } })
|
|
9
|
+
export class ScheduleImageDraftPrune extends BeanBase implements IScheduleExecute {
|
|
10
|
+
async execute() {
|
|
11
|
+
const items = await this.scope.model.image.select({
|
|
12
|
+
columns: ['id'],
|
|
13
|
+
where: {
|
|
14
|
+
status: 'draft',
|
|
15
|
+
draftExpiresAt: {
|
|
16
|
+
_lt_: new Date(),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
orders: [
|
|
20
|
+
['draftExpiresAt', 'asc'],
|
|
21
|
+
['id', 'asc'],
|
|
22
|
+
],
|
|
23
|
+
limit: PruneBatchLimit,
|
|
24
|
+
});
|
|
25
|
+
for (const item of items) {
|
|
26
|
+
await this.bean.image.expireDraftImage(item.id);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import type { IUploadFile } from 'vona-module-a-upload';
|
|
2
2
|
import type { IDecoratorControllerOptions } from 'vona-module-a-web';
|
|
3
3
|
|
|
4
|
-
import fse from 'fs-extra';
|
|
5
4
|
import { BeanBase } from 'vona';
|
|
6
5
|
import { Core } from 'vona-module-a-core';
|
|
7
6
|
import { Api, v } from 'vona-module-a-openapiutils';
|
|
7
|
+
import { Passport } from 'vona-module-a-user';
|
|
8
8
|
import { Arg, Controller, Web } from 'vona-module-a-web';
|
|
9
9
|
|
|
10
|
-
import type { IImageUploadTokenPayload } from '../types/image.ts';
|
|
11
|
-
|
|
12
10
|
import { DtoImageDeliveryRequest } from '../dto/imageDeliveryRequest.ts';
|
|
11
|
+
import { DtoImageDirectUploadFinalizeRequest } from '../dto/imageDirectUploadFinalizeRequest.ts';
|
|
12
|
+
import { DtoImageDirectUploadFinalizeResponse } from '../dto/imageDirectUploadFinalizeResponse.ts';
|
|
13
13
|
import { DtoImageDirectUploadRequest } from '../dto/imageDirectUploadRequest.ts';
|
|
14
14
|
import { DtoImageDirectUploadResponse } from '../dto/imageDirectUploadResponse.ts';
|
|
15
15
|
import { DtoImageUploadResponse } from '../dto/imageUploadResponse.ts';
|
|
@@ -36,7 +36,14 @@ export class ControllerImage extends BeanBase {
|
|
|
36
36
|
token,
|
|
37
37
|
this.ctx.route.routePathRaw,
|
|
38
38
|
);
|
|
39
|
-
await this.
|
|
39
|
+
await this.bean.imageUploadPolicy.validateUploadFile(
|
|
40
|
+
{
|
|
41
|
+
file: file.file,
|
|
42
|
+
filename: file.info.filename,
|
|
43
|
+
mimeType: file.info.mimeType,
|
|
44
|
+
},
|
|
45
|
+
payload,
|
|
46
|
+
);
|
|
40
47
|
const image = await this.bean.image.upload(
|
|
41
48
|
payload.providerName,
|
|
42
49
|
{
|
|
@@ -50,11 +57,7 @@ export class ControllerImage extends BeanBase {
|
|
|
50
57
|
imageScene: payload.imageScene,
|
|
51
58
|
},
|
|
52
59
|
);
|
|
53
|
-
return
|
|
54
|
-
...image,
|
|
55
|
-
url: await this.bean.image.getVariantUrl(image.id),
|
|
56
|
-
signed: !!image.requireSignedURLs,
|
|
57
|
-
};
|
|
60
|
+
return await this.bean.image.createImageActionResponse(image);
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
@Web.post('direct-upload')
|
|
@@ -70,6 +73,7 @@ export class ControllerImage extends BeanBase {
|
|
|
70
73
|
{
|
|
71
74
|
filename: data.filename,
|
|
72
75
|
contentType: data.contentType,
|
|
76
|
+
requireSignedURLs: data.requireSignedURLs,
|
|
73
77
|
expiry: data.expiry,
|
|
74
78
|
customId: data.customId,
|
|
75
79
|
},
|
|
@@ -81,6 +85,13 @@ export class ControllerImage extends BeanBase {
|
|
|
81
85
|
);
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
@Web.post('direct-upload/finalize')
|
|
89
|
+
@Api.body(DtoImageDirectUploadFinalizeResponse)
|
|
90
|
+
async finalizeDirectUpload(@Arg.body() data: DtoImageDirectUploadFinalizeRequest) {
|
|
91
|
+
const image = await this.bean.image.finalizeDirectUpload(data.imageId);
|
|
92
|
+
return await this.bean.image.createImageActionResponse(image);
|
|
93
|
+
}
|
|
94
|
+
|
|
84
95
|
@Web.post('upload-url')
|
|
85
96
|
@Api.body(DtoImageUploadResponse)
|
|
86
97
|
async uploadUrl(@Arg.body() data: DtoImageUploadUrlRequest) {
|
|
@@ -95,6 +106,7 @@ export class ControllerImage extends BeanBase {
|
|
|
95
106
|
url: data.url,
|
|
96
107
|
filename: data.filename,
|
|
97
108
|
contentType: data.contentType,
|
|
109
|
+
requireSignedURLs: data.requireSignedURLs,
|
|
98
110
|
},
|
|
99
111
|
{
|
|
100
112
|
clientName: policy.clientName,
|
|
@@ -102,64 +114,22 @@ export class ControllerImage extends BeanBase {
|
|
|
102
114
|
imageScene: policy.imageScene,
|
|
103
115
|
},
|
|
104
116
|
);
|
|
105
|
-
return
|
|
106
|
-
...image,
|
|
107
|
-
url: await this.bean.image.getVariantUrl(image.id),
|
|
108
|
-
signed: !!image.requireSignedURLs,
|
|
109
|
-
};
|
|
117
|
+
return await this.bean.image.createImageActionResponse(image);
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
@Web.get('delivery/:imageId')
|
|
121
|
+
@Passport.public()
|
|
113
122
|
async delivery(
|
|
114
123
|
@Arg.param('imageId', v.tableIdentity()) imageId: number,
|
|
115
124
|
@Arg.query(v.object(DtoImageDeliveryRequest)) query: DtoImageDeliveryRequest,
|
|
116
125
|
) {
|
|
117
126
|
const payload = await this.bean.imageUploadPolicy.verifyDeliveryToken(
|
|
118
127
|
query.token,
|
|
119
|
-
this.
|
|
128
|
+
this.scope.util.combineApiPath(`image/delivery/${imageId}`, false, true),
|
|
120
129
|
);
|
|
121
130
|
if (String(payload.imageId) !== String(imageId)) {
|
|
122
131
|
return this.app.throw(401);
|
|
123
132
|
}
|
|
124
133
|
this.ctx.redirect(payload.targetUrl);
|
|
125
134
|
}
|
|
126
|
-
|
|
127
|
-
private async _validateUploadFile(file: IUploadFile, payload: IImageUploadTokenPayload) {
|
|
128
|
-
const stat = await fse.stat(file.file);
|
|
129
|
-
const fileSize = Number(stat.size);
|
|
130
|
-
if (fileSize !== payload.fileSize) {
|
|
131
|
-
return this.app.throw(403, `image size mismatch: size=${fileSize}`);
|
|
132
|
-
}
|
|
133
|
-
if (payload.maxSize && fileSize > payload.maxSize) {
|
|
134
|
-
return this.app.throw(403, `image too large: maxSize=${payload.maxSize}`);
|
|
135
|
-
}
|
|
136
|
-
const mimeType = file.info.mimeType.toLowerCase();
|
|
137
|
-
if (mimeType !== payload.mimeType) {
|
|
138
|
-
return this.app.throw(403, `image mimeType mismatch: mimeType=${mimeType}`);
|
|
139
|
-
}
|
|
140
|
-
if (payload.mimeTypes?.length && !this._matchesMimeType(mimeType, payload.mimeTypes)) {
|
|
141
|
-
return this.app.throw(403, `unsupported image mimeType: ${mimeType}`);
|
|
142
|
-
}
|
|
143
|
-
const extension = this._getExtension(file.info.filename);
|
|
144
|
-
if (payload.extensions?.length && extension && !payload.extensions.includes(extension)) {
|
|
145
|
-
return this.app.throw(403, `unsupported image extension: ${extension}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private _matchesMimeType(mimeType: string, mimeTypes: string[]) {
|
|
150
|
-
return mimeTypes.some(item => {
|
|
151
|
-
if (item === mimeType) return true;
|
|
152
|
-
if (item.endsWith('/*')) {
|
|
153
|
-
return mimeType.startsWith(`${item.slice(0, -1)}`);
|
|
154
|
-
}
|
|
155
|
-
return false;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private _getExtension(filename?: string) {
|
|
160
|
-
if (!filename) return '';
|
|
161
|
-
const index = filename.lastIndexOf('.');
|
|
162
|
-
if (index === -1) return '';
|
|
163
|
-
return filename.slice(index).toLowerCase();
|
|
164
|
-
}
|
|
165
135
|
}
|
package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeRequest.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TableIdentity } from 'table-identity';
|
|
2
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
3
|
+
|
|
4
|
+
import { Api, v } from 'vona-module-a-openapiutils';
|
|
5
|
+
import { Dto } from 'vona-module-a-web';
|
|
6
|
+
|
|
7
|
+
export interface IDtoOptionsImageDirectUploadFinalizeRequest extends IDecoratorDtoOptions {}
|
|
8
|
+
|
|
9
|
+
@Dto<IDtoOptionsImageDirectUploadFinalizeRequest>()
|
|
10
|
+
export class DtoImageDirectUploadFinalizeRequest {
|
|
11
|
+
@Api.field(v.tableIdentity())
|
|
12
|
+
imageId: TableIdentity;
|
|
13
|
+
}
|
package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeResponse.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IDecoratorDtoOptions } from 'vona-module-a-web';
|
|
2
|
+
|
|
3
|
+
import { Dto } from 'vona-module-a-web';
|
|
4
|
+
|
|
5
|
+
import { DtoImageUploadResponse } from './imageUploadResponse.ts';
|
|
6
|
+
|
|
7
|
+
export interface IDtoOptionsImageDirectUploadFinalizeResponse extends IDecoratorDtoOptions {}
|
|
8
|
+
|
|
9
|
+
@Dto<IDtoOptionsImageDirectUploadFinalizeResponse>()
|
|
10
|
+
export class DtoImageDirectUploadFinalizeResponse extends DtoImageUploadResponse {}
|
|
@@ -5,6 +5,7 @@ import { Api, v } from 'vona-module-a-openapiutils';
|
|
|
5
5
|
import { Dto } from 'vona-module-a-web';
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
|
|
8
|
+
import type { TypeImageStatus } from '../types/image.ts';
|
|
8
9
|
import type { IImageProviderRecord } from '../types/imageProvider.ts';
|
|
9
10
|
import type { IImageSceneRecord } from '../types/imageScene.ts';
|
|
10
11
|
|
|
@@ -33,6 +34,15 @@ export class DtoImageDirectUploadResponse {
|
|
|
33
34
|
@Api.field(v.optional())
|
|
34
35
|
filename?: string;
|
|
35
36
|
|
|
37
|
+
@Api.field(v.optional())
|
|
38
|
+
requireSignedURLs?: boolean;
|
|
39
|
+
|
|
40
|
+
@Api.field(v.optional(), z.string())
|
|
41
|
+
status?: TypeImageStatus;
|
|
42
|
+
|
|
43
|
+
@Api.field(v.optional())
|
|
44
|
+
draftExpiresAt?: Date;
|
|
45
|
+
|
|
36
46
|
@Api.field(v.optional(), z.string())
|
|
37
47
|
imageScene?: keyof IImageSceneRecord;
|
|
38
48
|
}
|
|
@@ -5,7 +5,7 @@ import { $schema, Api, v } from 'vona-module-a-openapiutils';
|
|
|
5
5
|
import { Dto } from 'vona-module-a-web';
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
|
|
8
|
-
import type { IImageNamedVariants } from '../types/image.ts';
|
|
8
|
+
import type { IImageNamedVariants, TypeImageStatus } from '../types/image.ts';
|
|
9
9
|
import type { IImageProviderRecord } from '../types/imageProvider.ts';
|
|
10
10
|
import type { IImageSceneRecord } from '../types/imageScene.ts';
|
|
11
11
|
|
|
@@ -51,6 +51,15 @@ export class DtoImageUploadResponse {
|
|
|
51
51
|
@Api.field(v.optional(), z.string())
|
|
52
52
|
imageScene?: keyof IImageSceneRecord;
|
|
53
53
|
|
|
54
|
+
@Api.field(v.optional(), z.string())
|
|
55
|
+
status?: TypeImageStatus;
|
|
56
|
+
|
|
57
|
+
@Api.field(v.optional())
|
|
58
|
+
draftExpiresAt?: Date;
|
|
59
|
+
|
|
60
|
+
@Api.field(v.optional())
|
|
61
|
+
finalizedAt?: Date;
|
|
62
|
+
|
|
54
63
|
@Api.field(v.optional())
|
|
55
64
|
uploadedAt?: Date;
|
|
56
65
|
|
|
@@ -5,7 +5,7 @@ import { $schema, Api, v } from 'vona-module-a-openapiutils';
|
|
|
5
5
|
import { Dto } from 'vona-module-a-web';
|
|
6
6
|
import z from 'zod';
|
|
7
7
|
|
|
8
|
-
import type { IImageNamedVariants } from '../types/image.ts';
|
|
8
|
+
import type { IImageNamedVariants, TypeImageStatus } from '../types/image.ts';
|
|
9
9
|
import type { IImageSceneRecord } from '../types/imageScene.ts';
|
|
10
10
|
|
|
11
11
|
import { DtoImageTransformOptions } from './imageTransformOptions.tsx';
|
|
@@ -38,6 +38,15 @@ export class DtoImageView {
|
|
|
38
38
|
@Api.field(v.optional())
|
|
39
39
|
imageScene?: keyof IImageSceneRecord | string;
|
|
40
40
|
|
|
41
|
+
@Api.field(v.optional(), z.string())
|
|
42
|
+
status?: TypeImageStatus;
|
|
43
|
+
|
|
44
|
+
@Api.field(v.optional())
|
|
45
|
+
draftExpiresAt?: Date;
|
|
46
|
+
|
|
47
|
+
@Api.field(v.optional())
|
|
48
|
+
finalizedAt?: Date;
|
|
49
|
+
|
|
41
50
|
@Api.field(v.optional())
|
|
42
51
|
uploadedAt?: Date;
|
|
43
52
|
|
|
@@ -4,7 +4,7 @@ import { $schema, Api, v } from 'vona-module-a-openapiutils';
|
|
|
4
4
|
import { Entity, EntityBase } from 'vona-module-a-orm';
|
|
5
5
|
import z from 'zod';
|
|
6
6
|
|
|
7
|
-
import type { IImageNamedVariants } from '../types/image.ts';
|
|
7
|
+
import type { IImageNamedVariants, TypeImageStatus } from '../types/image.ts';
|
|
8
8
|
import type { IImageProviderRecord } from '../types/imageProvider.ts';
|
|
9
9
|
import type { IImageSceneRecord } from '../types/imageScene.ts';
|
|
10
10
|
|
|
@@ -55,4 +55,13 @@ export class EntityImage extends EntityBase {
|
|
|
55
55
|
|
|
56
56
|
@Api.field(v.optional(), z.string())
|
|
57
57
|
imageScene?: keyof IImageSceneRecord;
|
|
58
|
+
|
|
59
|
+
@Api.field(v.optional(), z.string())
|
|
60
|
+
status?: TypeImageStatus;
|
|
61
|
+
|
|
62
|
+
@Api.field(v.optional())
|
|
63
|
+
draftExpiresAt?: Date;
|
|
64
|
+
|
|
65
|
+
@Api.field(v.optional())
|
|
66
|
+
finalizedAt?: Date;
|
|
58
67
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
export function matchesImageMimeType(mimeType: string, mimeTypes: string[]) {
|
|
4
|
+
return mimeTypes.some(item => {
|
|
5
|
+
if (item === mimeType) return true;
|
|
6
|
+
if (item.endsWith('/*')) {
|
|
7
|
+
return mimeType.startsWith(`${item.slice(0, -1)}`);
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getImageExtension(filename?: string) {
|
|
14
|
+
return path.extname(filename ?? '').toLowerCase();
|
|
15
|
+
}
|
|
@@ -39,6 +39,8 @@ export type IImageNamedVariants = Partial<Record<TypeImageVariantName, IImageTra
|
|
|
39
39
|
|
|
40
40
|
export type TypeImageDeliveryExpiry = Date | string | number;
|
|
41
41
|
|
|
42
|
+
export type TypeImageStatus = 'draft' | 'ready' | 'expired';
|
|
43
|
+
|
|
42
44
|
export interface IImageDeliveryOptions {
|
|
43
45
|
signed?: boolean;
|
|
44
46
|
expiresIn?: number;
|
|
@@ -142,6 +144,9 @@ export interface IImageResource<
|
|
|
142
144
|
provider: keyof IImageProviderRecord;
|
|
143
145
|
clientName: string;
|
|
144
146
|
imageScene?: keyof IImageSceneRecord | string;
|
|
147
|
+
status?: TypeImageStatus;
|
|
148
|
+
draftExpiresAt?: Date;
|
|
149
|
+
finalizedAt?: Date;
|
|
145
150
|
uploadedAt?: Date;
|
|
146
151
|
}
|
|
147
152
|
|
|
@@ -153,6 +158,11 @@ export interface IImageDirectUploadResult<
|
|
|
153
158
|
draft?: boolean;
|
|
154
159
|
}
|
|
155
160
|
|
|
161
|
+
export interface IImageFinalizeDirectUploadResult<
|
|
162
|
+
TMeta extends TypeImageMeta = TypeImageMeta,
|
|
163
|
+
TRaw = unknown,
|
|
164
|
+
> extends IImageResource<TMeta, TRaw> {}
|
|
165
|
+
|
|
156
166
|
export interface IImageView {
|
|
157
167
|
id: TableIdentity;
|
|
158
168
|
url: string;
|
|
@@ -162,6 +172,9 @@ export interface IImageView {
|
|
|
162
172
|
provider: keyof IImageProviderRecord;
|
|
163
173
|
clientName: string;
|
|
164
174
|
imageScene?: keyof IImageSceneRecord | string;
|
|
175
|
+
status?: TypeImageStatus;
|
|
176
|
+
draftExpiresAt?: Date;
|
|
177
|
+
finalizedAt?: Date;
|
|
165
178
|
uploadedAt?: Date;
|
|
166
179
|
variants?: IImageNamedVariants;
|
|
167
180
|
signed?: boolean;
|
|
@@ -194,6 +207,11 @@ export interface IImageUploadTokenPayload<
|
|
|
194
207
|
kind: 'imageUpload';
|
|
195
208
|
}
|
|
196
209
|
|
|
210
|
+
export interface IImageDirectUploadTokenPayload {
|
|
211
|
+
kind: 'imageDirectUpload';
|
|
212
|
+
resourceId: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
197
215
|
export interface IImageDeliveryTokenPayload {
|
|
198
216
|
kind: 'imageDelivery';
|
|
199
217
|
imageId: TableIdentity;
|
|
@@ -37,16 +37,37 @@ export type TypeImageProviderClientOptions<T> =
|
|
|
37
37
|
T extends IDecoratorImageProviderOptions<any, infer O> ? O : never;
|
|
38
38
|
|
|
39
39
|
export type TypeImageProviderClientName<T> =
|
|
40
|
-
T extends IDecoratorImageProviderOptions<infer R, any> ? keyof R : never;
|
|
40
|
+
T extends IDecoratorImageProviderOptions<infer R, any> ? keyof R & string : never;
|
|
41
41
|
|
|
42
42
|
export interface IDecoratorImageProviderOptions<
|
|
43
43
|
R extends IImageProviderClientRecord = IImageProviderClientRecord,
|
|
44
44
|
T extends IImageProviderClientOptions = IImageProviderClientOptions,
|
|
45
45
|
> extends TypeOnionOptionsEnableSimple {
|
|
46
46
|
base?: T;
|
|
47
|
-
clients?: { [K in keyof R]?: T };
|
|
47
|
+
clients?: { [K in keyof R]?: R[K] extends undefined ? T : R[K] };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export type TypeImageProviderOptionsByName<N extends keyof IImageProviderRecord> =
|
|
51
|
+
IImageProviderRecord[N];
|
|
52
|
+
|
|
53
|
+
export type TypeImageProviderClientOptionsByName<N extends keyof IImageProviderRecord> =
|
|
54
|
+
TypeImageProviderOptionsByName<N> extends IDecoratorImageProviderOptions<any, any>
|
|
55
|
+
? TypeImageProviderClientOptions<TypeImageProviderOptionsByName<N>>
|
|
56
|
+
: never;
|
|
57
|
+
|
|
58
|
+
export type TypeImageProviderClientNameByName<N extends keyof IImageProviderRecord> =
|
|
59
|
+
TypeImageProviderOptionsByName<N> extends IDecoratorImageProviderOptions<any, any>
|
|
60
|
+
? TypeImageProviderClientName<TypeImageProviderOptionsByName<N>>
|
|
61
|
+
: never;
|
|
62
|
+
|
|
63
|
+
export type TypeImageProviderExecuteByName<N extends keyof IImageProviderRecord> =
|
|
64
|
+
TypeImageProviderOptionsByName<N> extends IDecoratorImageProviderOptions<any, any>
|
|
65
|
+
? IImageProviderExecute<
|
|
66
|
+
TypeImageProviderClientOptionsByName<N>,
|
|
67
|
+
TypeImageProviderOptionsByName<N>
|
|
68
|
+
>
|
|
69
|
+
: never;
|
|
70
|
+
|
|
50
71
|
// Provider-internal resolved variant names include both declaration-merged named variants
|
|
51
72
|
// and the internal `custom` sentinel used for ad hoc transform requests.
|
|
52
73
|
export type TypeImageProviderResolvedVariantName = TypeImageVariantName | 'custom';
|
|
@@ -71,6 +92,11 @@ export interface IImageProviderExecute<
|
|
|
71
92
|
clientOptions: T,
|
|
72
93
|
options: O,
|
|
73
94
|
): Promise<IImageProviderDirectUploadResource>;
|
|
95
|
+
finalizeDirectUpload?(
|
|
96
|
+
image: EntityImage,
|
|
97
|
+
clientOptions: T,
|
|
98
|
+
options: O,
|
|
99
|
+
): Promise<IImageProviderResource | undefined>;
|
|
74
100
|
get(
|
|
75
101
|
image: EntityImage,
|
|
76
102
|
clientOptions: T,
|