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
@@ -43,7 +43,11 @@ export interface IImageProviderOptionsCloudflare extends IDecoratorImageProvider
43
43
  },
44
44
  },
45
45
  })
46
- export class ImageProviderCloudflare extends BeanBase implements IImageProviderExecute {
46
+ export class ImageProviderCloudflare
47
+ extends BeanBase
48
+ implements
49
+ IImageProviderExecute<IImageProviderCloudflareClientOptions, IImageProviderOptionsCloudflare>
50
+ {
47
51
  async upload(
48
52
  input: IImageUploadInput,
49
53
  clientOptions: IImageProviderCloudflareClientOptions,
@@ -68,6 +72,14 @@ export class ImageProviderCloudflare extends BeanBase implements IImageProviderE
68
72
  return await this.scope.service.imageCloudflare.createDirectUpload(input, clientOptions);
69
73
  }
70
74
 
75
+ async finalizeDirectUpload(
76
+ image: EntityImage,
77
+ clientOptions: IImageProviderCloudflareClientOptions,
78
+ _options: IImageProviderOptionsCloudflare,
79
+ ) {
80
+ return await this.scope.service.imageCloudflare.finalizeDirectUpload(image, clientOptions);
81
+ }
82
+
71
83
  async get(
72
84
  image: EntityImage,
73
85
  clientOptions: IImageProviderCloudflareClientOptions,
@@ -38,6 +38,12 @@ interface ICloudflareImageResult {
38
38
  draft?: boolean;
39
39
  }
40
40
 
41
+ interface ICloudflareImageDetailsResult extends ICloudflareImageResult {
42
+ meta?: {
43
+ filename?: string;
44
+ };
45
+ }
46
+
41
47
  interface ICloudflareClientOptionsNormalized extends IImageProviderCloudflareClientOptions {
42
48
  accountId: string;
43
49
  apiToken: string;
@@ -130,6 +136,26 @@ export class ServiceImageCloudflare extends BeanBase {
130
136
  };
131
137
  }
132
138
 
139
+ async finalizeDirectUpload(
140
+ image: Pick<EntityImage, 'resourceId' | 'filename' | 'meta' | 'requireSignedURLs'>,
141
+ options: IImageProviderCloudflareClientOptions,
142
+ ): Promise<IImageProviderResource | undefined> {
143
+ const normalized = this._normalizeClientOptions(options);
144
+ const result = await this._request<ICloudflareImageDetailsResult>(
145
+ normalized,
146
+ `/images/v1/${encodeURIComponent(image.resourceId)}`,
147
+ {
148
+ method: 'GET',
149
+ },
150
+ );
151
+ if (result.draft) return undefined;
152
+ return this._mapImageResource(result, normalized, {
153
+ filename: result.filename ?? result.meta?.filename ?? image.filename,
154
+ meta: image.meta,
155
+ requireSignedURLs: image.requireSignedURLs,
156
+ });
157
+ }
158
+
133
159
  async remove(
134
160
  image: Pick<EntityImage, 'resourceId'>,
135
161
  options: IImageProviderCloudflareClientOptions,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-image-native",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -46,7 +46,11 @@
46
46
  "title": "image-native",
47
47
  "vonaModule": {
48
48
  "dependencies": {
49
- "a-image": "5.0.0"
49
+ "a-core": "5.0.0",
50
+ "a-image": "5.0.0",
51
+ "a-upload": "5.0.0",
52
+ "a-user": "5.0.0",
53
+ "a-web": "5.0.0"
50
54
  },
51
55
  "globalDependencies": {
52
56
  "sharp": true
@@ -1,4 +1,5 @@
1
1
  // eslint-disable
2
+ import type { TypeControllerOptionsActions } from 'vona-module-a-openapi';
2
3
  /** service: begin */
3
4
  export * from '../service/imageNative.ts';
4
5
 
@@ -72,6 +73,42 @@ export interface IModuleImageProvider {
72
73
  'native': ImageProviderNative;
73
74
  }
74
75
  /** imageProvider: end */
76
+ /** controller: begin */
77
+ export * from '../controller/image.ts';
78
+ import type { IControllerOptionsImage } from '../controller/image.ts';
79
+ import 'vona-module-a-web';
80
+ declare module 'vona-module-a-web' {
81
+ export interface IControllerRecord {
82
+ 'image-native:image': IControllerOptionsImage;
83
+ }
84
+ }
85
+ declare module 'vona-module-image-native' {
86
+ export interface ControllerImage {
87
+ /** @internal */
88
+ get scope(): ScopeModuleImageNative;
89
+ }
90
+
91
+ export interface ControllerImage {
92
+ get $beanFullName(): 'image-native.controller.image';
93
+ get $onionName(): 'image-native:image';
94
+ get $onionOptions(): IControllerOptionsImage;
95
+ }
96
+ }
97
+ /** controller: end */
98
+ /** controller: begin */
99
+ // @ts-ignore ignore
100
+ import type { ControllerImage } from '../controller/image.ts';
101
+ declare module 'vona-module-image-native' {
102
+ export interface IControllerOptionsImage {
103
+ actions?: TypeControllerOptionsActions<ControllerImage>;
104
+ }
105
+ }
106
+ declare module 'vona-module-a-web' {
107
+ export interface IApiPathPostRecord {
108
+ '/image-native/direct-upload/:resourceId': undefined;
109
+ }
110
+ }
111
+ /** controller: end */
75
112
  /** config: begin */
76
113
  export * from '../config/config.ts';
77
114
  import type { config } from '../config/config.ts';
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  IDecoratorImageProviderOptions,
3
3
  IImageDeliveryOptions,
4
+ IImageDirectUploadInput,
4
5
  IImageDownloadResult,
5
6
  IImageProviderClientOptions,
6
7
  IImageProviderClientRecord,
@@ -34,7 +35,10 @@ export interface IImageProviderOptionsNative extends IDecoratorImageProviderOpti
34
35
  signedDeliveryKind: 'proxy',
35
36
  },
36
37
  })
37
- export class ImageProviderNative extends BeanBase implements IImageProviderExecute {
38
+ export class ImageProviderNative
39
+ extends BeanBase
40
+ implements IImageProviderExecute<IImageProviderNativeClientOptions, IImageProviderOptionsNative>
41
+ {
38
42
  async upload(
39
43
  input: IImageUploadInput,
40
44
  clientOptions: IImageProviderNativeClientOptions,
@@ -49,6 +53,22 @@ export class ImageProviderNative extends BeanBase implements IImageProviderExecu
49
53
  };
50
54
  }
51
55
 
56
+ async createDirectUpload(
57
+ input: IImageDirectUploadInput,
58
+ clientOptions: IImageProviderNativeClientOptions,
59
+ _options: IImageProviderOptionsNative,
60
+ ) {
61
+ return await this.scope.service.imageNative.createDirectUpload(input, clientOptions);
62
+ }
63
+
64
+ async finalizeDirectUpload(
65
+ image: EntityImage,
66
+ clientOptions: IImageProviderNativeClientOptions,
67
+ _options: IImageProviderOptionsNative,
68
+ ) {
69
+ return await this.scope.service.imageNative.finalizeDirectUpload(image, clientOptions);
70
+ }
71
+
52
72
  async get(
53
73
  image: EntityImage,
54
74
  clientOptions: IImageProviderNativeClientOptions,
@@ -0,0 +1,57 @@
1
+ import type { IUploadFile } from 'vona-module-a-upload';
2
+ import type { IDecoratorControllerOptions } from 'vona-module-a-web';
3
+
4
+ import fse from 'fs-extra';
5
+ import { BeanBase } from 'vona';
6
+ import { Core } from 'vona-module-a-core';
7
+ import { Passport } from 'vona-module-a-user';
8
+ import { Arg, Controller, Web } from 'vona-module-a-web';
9
+
10
+ export interface IControllerOptionsImage extends IDecoratorControllerOptions {}
11
+
12
+ @Controller<IControllerOptionsImage>({ path: 'image-native' })
13
+ export class ControllerImage extends BeanBase {
14
+ @Web.post('direct-upload/:resourceId')
15
+ @Passport.public()
16
+ @Core.fileUpload()
17
+ async directUpload(
18
+ @Arg.param('resourceId') resourceId: string,
19
+ @Arg.query('token') token: string,
20
+ @Arg.file('image') file: IUploadFile,
21
+ ) {
22
+ const payload = await this.bean.imageUploadPolicy.verifyDirectUploadToken(
23
+ token,
24
+ this.ctx.route.routePathRaw,
25
+ );
26
+ if (payload.resourceId !== resourceId) {
27
+ return this.app.throw(401);
28
+ }
29
+ const modelImage = this.app.bean._getBean<any>('a-image.model.image' as never);
30
+ const image = await modelImage.get({
31
+ providerName: 'image-native:native',
32
+ resourceId,
33
+ });
34
+ if (!image || image.status !== 'draft') {
35
+ return this.app.throw(403, `image is not draft: ${resourceId}`);
36
+ }
37
+ if (!image.imageScene) {
38
+ throw new Error(`image scene missing: ${resourceId}`);
39
+ }
40
+ const stat = await fse.stat(file.file);
41
+ const policy = await this.bean.imageUploadPolicy.resolveUploadPolicy({
42
+ imageScene: image.imageScene,
43
+ size: Number(stat.size),
44
+ mimeType: file.info.mimeType,
45
+ });
46
+ await this.bean.imageUploadPolicy.validateUploadFile(
47
+ {
48
+ file: file.file,
49
+ filename: file.info.filename,
50
+ mimeType: file.info.mimeType,
51
+ },
52
+ policy,
53
+ );
54
+ await this.scope.service.imageNative.uploadDirectFile(image, file);
55
+ return { ok: true };
56
+ }
57
+ }
@@ -2,13 +2,15 @@ import type { SharpConstructor } from 'sharp';
2
2
  import type {
3
3
  EntityImage,
4
4
  IImageDeliveryOptions,
5
- IImageNamedVariants,
5
+ IImageDirectUploadInput,
6
+ IImageProviderDirectUploadResource,
6
7
  IImageProviderResolvedVariant,
7
8
  IImageProviderResource,
8
9
  IImageTransformOptions,
9
10
  IImageUploadInput,
10
11
  IImageVariantRequest,
11
12
  } from 'vona-module-a-image';
13
+ import type { IUploadFile } from 'vona-module-a-upload';
12
14
 
13
15
  import fse from 'fs-extra';
14
16
  import { createHash } from 'node:crypto';
@@ -24,6 +26,20 @@ type IImageNativeStoredImage = Pick<
24
26
  'resourceId' | 'filename' | 'storagePath' | 'variants'
25
27
  >;
26
28
 
29
+ type IImageNativeDraftImage = Pick<
30
+ EntityImage,
31
+ | 'resourceId'
32
+ | 'filename'
33
+ | 'contentType'
34
+ | 'meta'
35
+ | 'imageScene'
36
+ | 'requireSignedURLs'
37
+ | 'storagePath'
38
+ | 'deliveryBaseUrl'
39
+ | 'variants'
40
+ | 'clientName'
41
+ >;
42
+
27
43
  @Service()
28
44
  export class ServiceImageNative extends BeanBase {
29
45
  private _sharpInstance: SharpConstructor;
@@ -39,39 +55,90 @@ export class ServiceImageNative extends BeanBase {
39
55
  input: IImageUploadInput,
40
56
  options: IImageProviderNativeClientOptions,
41
57
  ): Promise<IImageProviderResource> {
42
- const publicPath = await this.app.util.getPublicPathPhysical(
43
- path.join('image-native', options.subdir ?? 'default'),
44
- true,
45
- );
46
58
  const resourceId = uuidv4();
47
- const ext = this._getOriginalExt(input.filename, input.file);
48
- const targetName = `${resourceId}${ext}`;
49
- const targetPath = path.join(publicPath, targetName);
59
+ const targetPath = await this._getFinalPath(resourceId, input.filename, options, input.file);
50
60
  await fse.copy(input.file, targetPath);
51
- const sharp = await this.getSharp();
52
- const metadata = await sharp(targetPath).metadata();
53
- const stat = await fse.stat(targetPath);
54
- const variants = options.variants ?? this.scope.config.imageNative.variants;
55
- await this._generateNamedVariants(
56
- { resourceId, filename: input.filename, storagePath: targetPath, variants },
57
- options,
61
+ return await this._buildStoredResource(targetPath, {
62
+ resourceId,
63
+ filename: input.filename,
64
+ contentType: input.contentType,
65
+ meta: input.meta,
66
+ requireSignedURLs: input.requireSignedURLs ?? options.requireSignedURLs,
67
+ variants: options.variants ?? this.scope.config.imageNative.variants,
68
+ deliveryBaseUrl: options.deliveryBaseUrl,
69
+ });
70
+ }
71
+
72
+ async createDirectUpload(
73
+ input: IImageDirectUploadInput,
74
+ options: IImageProviderNativeClientOptions,
75
+ ): Promise<IImageProviderDirectUploadResource> {
76
+ const resourceId = this._sanitizeResourceId(input.customId) ?? uuidv4();
77
+ const storagePath = await this._getFinalPath(resourceId, input.filename, options);
78
+ const routePath = this.scope.util.combineApiPath(
79
+ `image-native/direct-upload/${encodeURIComponent(resourceId)}`,
80
+ false,
81
+ true,
58
82
  );
83
+ const tokenPath = this.scope.util.combineApiPath(
84
+ 'image-native/direct-upload/:resourceId',
85
+ false,
86
+ true,
87
+ );
88
+ const tokenPayload = await this.bean.imageUploadPolicy.createDirectUploadToken({
89
+ resourceId,
90
+ path: tokenPath,
91
+ });
92
+ const uploadUrl = new URL(this.app.util.getAbsoluteUrlByApiPath(routePath));
93
+ uploadUrl.searchParams.set('token', tokenPayload.token);
59
94
  return {
60
95
  resourceId,
61
- size: Number(stat.size),
62
- width: metadata.width,
63
- height: metadata.height,
64
- storagePath: targetPath,
65
- deliveryBaseUrl: options.deliveryBaseUrl,
96
+ filename: input.filename,
97
+ contentType: input.contentType,
98
+ meta: input.meta,
66
99
  requireSignedURLs: input.requireSignedURLs ?? options.requireSignedURLs,
67
- variants,
100
+ variants: options.variants ?? this.scope.config.imageNative.variants,
101
+ deliveryBaseUrl: options.deliveryBaseUrl,
102
+ storagePath,
103
+ uploadUrl: uploadUrl.toString(),
104
+ draft: true,
68
105
  };
69
106
  }
70
107
 
108
+ async uploadDirectFile(image: IImageNativeDraftImage, file: IUploadFile) {
109
+ const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
110
+ await fse.ensureDir(path.dirname(draftPath));
111
+ await fse.move(file.file, draftPath, { overwrite: true });
112
+ return draftPath;
113
+ }
114
+
115
+ async finalizeDirectUpload(
116
+ image: IImageNativeDraftImage,
117
+ options: IImageProviderNativeClientOptions,
118
+ ): Promise<IImageProviderResource | undefined> {
119
+ const storagePath =
120
+ image.storagePath ?? (await this._getFinalPath(image.resourceId, image.filename, options));
121
+ const draftPath = this._getDraftPath(storagePath, image.resourceId);
122
+ if (!(await fse.pathExists(draftPath))) return undefined;
123
+ await fse.ensureDir(path.dirname(storagePath));
124
+ await fse.move(draftPath, storagePath, { overwrite: true });
125
+ return await this._buildStoredResource(storagePath, {
126
+ resourceId: image.resourceId,
127
+ filename: image.filename,
128
+ contentType: image.contentType,
129
+ meta: image.meta,
130
+ requireSignedURLs: image.requireSignedURLs ?? options.requireSignedURLs,
131
+ variants: image.variants ?? options.variants ?? this.scope.config.imageNative.variants,
132
+ deliveryBaseUrl: image.deliveryBaseUrl ?? options.deliveryBaseUrl,
133
+ });
134
+ }
135
+
71
136
  async remove(image: IImageNativeStoredImage) {
72
137
  if (!image.storagePath) return;
73
138
  const dir = path.dirname(image.storagePath);
74
139
  if (!(await fse.pathExists(dir))) return;
140
+ const draftPath = this._getDraftPath(image.storagePath, image.resourceId);
141
+ await fse.remove(draftPath);
75
142
  const entries = await fse.readdir(dir);
76
143
  await Promise.all(
77
144
  entries
@@ -100,22 +167,57 @@ export class ServiceImageNative extends BeanBase {
100
167
  return await this._buildUrl(targetPath, options.deliveryBaseUrl);
101
168
  }
102
169
 
103
- private async _generateNamedVariants(
104
- image: IImageNativeStoredImage,
170
+ private async _buildStoredResource(
171
+ targetPath: string,
172
+ data: {
173
+ resourceId: string;
174
+ filename?: string;
175
+ contentType?: string;
176
+ meta?: Record<string, unknown>;
177
+ requireSignedURLs?: boolean;
178
+ variants?: EntityImage['variants'];
179
+ deliveryBaseUrl?: string;
180
+ },
181
+ ): Promise<IImageProviderResource> {
182
+ const sharp = await this.getSharp();
183
+ const metadata = await sharp(targetPath).metadata();
184
+ const stat = await fse.stat(targetPath);
185
+ return {
186
+ resourceId: data.resourceId,
187
+ filename: data.filename,
188
+ contentType: data.contentType,
189
+ size: Number(stat.size),
190
+ width: metadata.width,
191
+ height: metadata.height,
192
+ requireSignedURLs: data.requireSignedURLs,
193
+ variants: data.variants,
194
+ meta: data.meta,
195
+ storagePath: targetPath,
196
+ deliveryBaseUrl: data.deliveryBaseUrl,
197
+ };
198
+ }
199
+
200
+ private async _getFinalPath(
201
+ resourceId: string,
202
+ filename: string | undefined,
105
203
  options: IImageProviderNativeClientOptions,
204
+ filePath?: string,
106
205
  ) {
107
- const variantNames = Object.keys(image.variants ?? {}) as Array<
108
- keyof NonNullable<IImageNamedVariants>
109
- >;
110
- for (const variantName of variantNames) {
111
- if (variantName === 'original') continue;
112
- await this._ensureVariantFile(
113
- image,
114
- resolveImageVariantRequestToTransform({ variantName }, 'original', image.variants),
115
- );
206
+ const publicPath = await this.app.util.getPublicPathPhysical(
207
+ path.join('image-native', options.subdir ?? 'default'),
208
+ true,
209
+ );
210
+ const ext = this._getOriginalExt(filename, filePath ?? resourceId);
211
+ return path.join(publicPath, `${resourceId}${ext}`);
212
+ }
213
+
214
+ private _getDraftPath(storagePath: string | undefined, resourceId: string) {
215
+ if (!storagePath) {
216
+ throw new Error(`Image storage path missing: ${resourceId}`);
116
217
  }
117
- if (!image.variants?.original) return;
118
- await this.getVariantUrl(image, { variantName: 'original' }, options);
218
+ const dir = path.dirname(storagePath);
219
+ const ext = path.extname(storagePath) || '.bin';
220
+ return path.join(dir, `${resourceId}__draft${ext}`);
119
221
  }
120
222
 
121
223
  private async _ensureVariantFile(
@@ -266,6 +368,12 @@ export class ServiceImageNative extends BeanBase {
266
368
  return JSON.stringify(value);
267
369
  }
268
370
 
371
+ private _sanitizeResourceId(resourceId: string | undefined) {
372
+ if (!resourceId) return undefined;
373
+ const sanitized = resourceId.replace(/[^\w.-]/g, '-');
374
+ return sanitized || undefined;
375
+ }
376
+
269
377
  private _sanitizeVariantName(variantName: string) {
270
378
  return variantName.replace(/[^\w.-]/g, '-');
271
379
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-image",
3
- "version": "5.0.1",
3
+ "version": "5.0.4",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "files": [],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-orm",
3
- "version": "5.1.20",
3
+ "version": "5.1.23",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "db",
6
6
  "keywords": [
@@ -11,7 +11,5 @@ export type TypeEntityMeta<T, N = string> = Required<{
11
11
  export const SymbolKeyFieldsMore = Symbol('$fieldsMore');
12
12
  export type TypeSymbolKeyFieldsMore = typeof SymbolKeyFieldsMore;
13
13
 
14
- export type TypeEntityStudentMetaSimple<T> = Omit<T, '$table' | '$comment' | '$default'>;
15
- export type TypeEntityStudentMetaSimpleColumns<T> = PickString<
16
- keyof TypeEntityStudentMetaSimple<T>
17
- >;
14
+ export type TypeEntityMetaFields<T> = Omit<T, '$table' | '$comment' | '$default'>;
15
+ export type TypeEntityMetaFieldKeys<T> = PickString<keyof TypeEntityMetaFields<T>>;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-ormutils",
3
- "version": "5.1.11",
3
+ "version": "5.1.14",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -4,7 +4,7 @@ import type {
4
4
  IDecoratorEntityOptions,
5
5
  ITableRecord,
6
6
  TypeEntityMeta,
7
- TypeEntityStudentMetaSimpleColumns,
7
+ TypeEntityMetaFieldKeys,
8
8
  } from 'vona-module-a-orm';
9
9
 
10
10
  import { isClass } from '@cabloy/utils';
@@ -15,6 +15,9 @@ import {
15
15
  getTargetDecoratorRules,
16
16
  } from 'vona-module-a-openapiutils';
17
17
 
18
+ export type TypeEntityMetaCompositeFieldKeys<T> =
19
+ `${TypeEntityMetaFieldKeys<T>}+${TypeEntityMetaFieldKeys<T>}`;
20
+
18
21
  export function $column<T>(key: keyof T): keyof T {
19
22
  return key;
20
23
  }
@@ -53,8 +56,9 @@ export function $columnsAll<T, TableName extends boolean, Meta extends boolean>(
53
56
  export function $tableColumns<K extends keyof ITableRecord>(
54
57
  tableName: K,
55
58
  key?:
56
- | TypeEntityStudentMetaSimpleColumns<ITableRecord[K]>
57
- | TypeEntityStudentMetaSimpleColumns<ITableRecord[K]>[]
59
+ | TypeEntityMetaFieldKeys<ITableRecord[K]>
60
+ | TypeEntityMetaCompositeFieldKeys<ITableRecord[K]>
61
+ | TypeEntityMetaFieldKeys<ITableRecord[K]>[]
58
62
  | undefined,
59
63
  ) {
60
64
  return { [tableName]: key };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-vona",
3
- "version": "5.1.61",
3
+ "version": "5.1.64",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",