@solidxai/core 0.1.12-beta.2 → 0.1.12-beta.3

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 (128) hide show
  1. package/.claude/settings.local.json +16 -0
  2. package/CLAUDE.md +26 -0
  3. package/dist/constants/media-file-types.d.ts +5 -0
  4. package/dist/constants/media-file-types.d.ts.map +1 -0
  5. package/dist/constants/media-file-types.js +27 -0
  6. package/dist/constants/media-file-types.js.map +1 -0
  7. package/dist/controllers/media.controller.d.ts +14 -11
  8. package/dist/controllers/media.controller.d.ts.map +1 -1
  9. package/dist/controllers/media.controller.js +72 -33
  10. package/dist/controllers/media.controller.js.map +1 -1
  11. package/dist/dtos/create-media.dto.d.ts +1 -0
  12. package/dist/dtos/create-media.dto.d.ts.map +1 -1
  13. package/dist/dtos/create-media.dto.js +7 -1
  14. package/dist/dtos/create-media.dto.js.map +1 -1
  15. package/dist/dtos/update-media.dto.d.ts +1 -0
  16. package/dist/dtos/update-media.dto.d.ts.map +1 -1
  17. package/dist/dtos/update-media.dto.js +7 -1
  18. package/dist/dtos/update-media.dto.js.map +1 -1
  19. package/dist/entities/media.entity.d.ts +1 -0
  20. package/dist/entities/media.entity.d.ts.map +1 -1
  21. package/dist/entities/media.entity.js +9 -1
  22. package/dist/entities/media.entity.js.map +1 -1
  23. package/dist/enums/auth-type.enum.d.ts +2 -1
  24. package/dist/enums/auth-type.enum.d.ts.map +1 -1
  25. package/dist/enums/auth-type.enum.js +1 -0
  26. package/dist/enums/auth-type.enum.js.map +1 -1
  27. package/dist/guards/authentication.guard.d.ts +3 -1
  28. package/dist/guards/authentication.guard.d.ts.map +1 -1
  29. package/dist/guards/authentication.guard.js +5 -1
  30. package/dist/guards/authentication.guard.js.map +1 -1
  31. package/dist/guards/media-signed-url.guard.d.ts +9 -0
  32. package/dist/guards/media-signed-url.guard.d.ts.map +1 -0
  33. package/dist/guards/media-signed-url.guard.js +49 -0
  34. package/dist/guards/media-signed-url.guard.js.map +1 -0
  35. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
  36. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +10 -13
  37. package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
  38. package/dist/repository/media.repository.d.ts.map +1 -1
  39. package/dist/repository/media.repository.js +2 -0
  40. package/dist/repository/media.repository.js.map +1 -1
  41. package/dist/seeders/seed-data/solid-core-metadata.json +88 -3
  42. package/dist/services/media-download-url.service.d.ts +21 -0
  43. package/dist/services/media-download-url.service.d.ts.map +1 -0
  44. package/dist/services/media-download-url.service.js +67 -0
  45. package/dist/services/media-download-url.service.js.map +1 -0
  46. package/dist/services/media-storage.utils.d.ts +27 -0
  47. package/dist/services/media-storage.utils.d.ts.map +1 -0
  48. package/dist/services/media-storage.utils.js +82 -0
  49. package/dist/services/media-storage.utils.js.map +1 -0
  50. package/dist/services/media.service.d.ts +20 -4
  51. package/dist/services/media.service.d.ts.map +1 -1
  52. package/dist/services/media.service.js +157 -55
  53. package/dist/services/media.service.js.map +1 -1
  54. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +4 -4
  55. package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
  56. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +37 -63
  57. package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
  58. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +5 -5
  59. package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
  60. package/dist/services/mediaStorageProviders/file-storage-provider.js +37 -38
  61. package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
  62. package/dist/solid-core.module.d.ts.map +1 -1
  63. package/dist/solid-core.module.js +11 -1
  64. package/dist/solid-core.module.js.map +1 -1
  65. package/dist/subscribers/audit.subscriber.js +1 -1
  66. package/dist/subscribers/audit.subscriber.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/constants/media-file-types.ts +44 -0
  69. package/src/controllers/media.controller.ts +44 -24
  70. package/src/dtos/create-media.dto.ts +8 -3
  71. package/src/dtos/update-media.dto.ts +8 -3
  72. package/src/entities/media.entity.ts +4 -1
  73. package/src/enums/auth-type.enum.ts +1 -0
  74. package/src/guards/authentication.guard.ts +3 -0
  75. package/src/guards/media-signed-url.guard.ts +41 -0
  76. package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +12 -24
  77. package/src/repository/media.repository.ts +2 -0
  78. package/src/seeders/seed-data/solid-core-metadata.json +88 -3
  79. package/src/services/1.js +6 -0
  80. package/src/services/media-download-url.service.ts +67 -0
  81. package/src/services/media-storage.utils.ts +79 -0
  82. package/src/services/media.service.ts +189 -59
  83. package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +52 -76
  84. package/src/services/mediaStorageProviders/file-storage-provider.ts +44 -48
  85. package/src/solid-core.module.ts +21 -2
  86. package/src/subscribers/audit.subscriber.ts +1 -1
  87. package/dist-tests/api/authenticate.spec.js +0 -119
  88. package/dist-tests/api/authenticate.spec.js.map +0 -1
  89. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  90. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  91. package/dist-tests/api/ping.spec.js +0 -21
  92. package/dist-tests/api/ping.spec.js.map +0 -1
  93. package/dist-tests/helpers/auth.js +0 -41
  94. package/dist-tests/helpers/auth.js.map +0 -1
  95. package/dist-tests/helpers/env.js +0 -11
  96. package/dist-tests/helpers/env.js.map +0 -1
  97. package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
  98. package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
  99. package/docs/agent-hub-grooming.md +0 -301
  100. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
  101. package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
  102. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
  103. package/docs/datasource-introspection-ddl-analysis.md +0 -326
  104. package/docs/datasource-introspection-implementation-plan.md +0 -306
  105. package/docs/grouping-enhancements.md +0 -89
  106. package/docs/java-spring/README.md +0 -3
  107. package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
  108. package/docs/module-package-import-handoff.md +0 -691
  109. package/docs/seed-changes.md +0 -65
  110. package/docs/test-data-workflow.md +0 -200
  111. package/docs/type-declaration-import-issue.md +0 -24
  112. package/docs/workflow/business-automation-example-notes.md +0 -309
  113. package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
  114. package/docs/workflow/foreach-example-notes.md +0 -187
  115. package/docs/workflow/hello-world-example-notes.md +0 -271
  116. package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
  117. package/docs/workflow/loop-until-example-notes.md +0 -198
  118. package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
  119. package/docs/workflow/samples.md +0 -25
  120. package/docs/workflow/what-is-kestra.md +0 -79
  121. package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
  122. package/docs/workflow/workflow-module-brd.md +0 -185
  123. package/docs/workflow/workflow-module-domain-model.md +0 -252
  124. package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
  125. package/docs/workflow/workflow-module-technical-spec.md +0 -293
  126. package/docs/workflow/workflow-node-type-contracts.md +0 -635
  127. package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
  128. package/docs/workflow/workflow-solidx-model-field-spec.md +0 -284
@@ -1,33 +1,37 @@
1
- import { forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common';
1
+ import { forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
2
2
  import { ModuleRef } from "@nestjs/core";
3
3
  import { InjectEntityManager } from '@nestjs/typeorm';
4
4
  import { EntityManager, In } from 'typeorm';
5
5
  import * as path from 'path';
6
- import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from "src/services/settings/default-settings-provider.service";
7
- import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service";
6
+ import { Readable } from 'stream';
8
7
 
9
8
  import { ConfigService } from '@nestjs/config';
10
9
  import { CRUDService } from 'src/services/crud.service';
11
10
  import { DiskFileService, S3FileService } from 'src/services/file';
11
+ import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
12
12
 
13
13
 
14
14
  import { ERROR_MESSAGES } from 'src/constants/error-messages';
15
15
  import { BasicFilterDto } from 'src/dtos/basic-filters.dto';
16
16
  import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
17
17
  import { Media } from 'src/entities/media.entity';
18
+ import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
18
19
  import { FieldMetadataRepository } from 'src/repository/field-metadata.repository';
19
20
  import { MediaStorageProviderMetadataRepository } from 'src/repository/media-storage-provider-metadata.repository';
20
21
  import { MediaRepository } from 'src/repository/media.repository';
21
22
  import { ModelMetadataRepository } from 'src/repository/model-metadata.repository';
23
+ import { buildDiskMediaPath, buildStoredMediaFileName, getEffectiveS3Region, resolveStoredMediaIsPublic, } from 'src/services/media-storage.utils';
22
24
  import { getMediaStorageProvider } from "./mediaStorageProviders";
23
25
 
24
-
25
26
  @Injectable()
26
27
  export class MediaService extends CRUDService<Media> {
28
+ private readonly logger = new Logger(MediaService.name);
29
+
27
30
  constructor(
28
31
  readonly configService: ConfigService,
29
32
  readonly diskFileService: DiskFileService,
30
33
  readonly s3FileService: S3FileService,
34
+ private readonly mediaDownloadUrlService: MediaDownloadUrlService,
31
35
  @InjectEntityManager()
32
36
  readonly entityManager: EntityManager,
33
37
  // @InjectRepository(Media, 'default')
@@ -51,75 +55,73 @@ export class MediaService extends CRUDService<Media> {
51
55
  async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}) {
52
56
  const data = await super.find(basicFilterDto, solidRequestContext);
53
57
  if (data.records) {
54
-
55
- for (const media of data.records) {
56
- const mediaStorageProvider = media.mediaStorageProviderMetadata;
57
-
58
- if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
59
- media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
60
- } else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
61
- media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
62
- }
63
- }
58
+ await this.decorateMediaRecords(data.records);
64
59
  }
65
60
  if (data.groupRecords) {
66
-
67
61
  for (const group of data.groupRecords) {
68
- for (const media of group.groupData.records) {
69
- const mediaStorageProvider = media.mediaStorageProviderMetadata;
70
-
71
- if (mediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
72
- media.relativeUri = await this.diskFileService.getUrl(this.getFullFilePathForDisk(media.relativeUri));
73
- }
74
- else if (mediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
75
- media.relativeUri = await this.s3FileService.getUrl(`${mediaStorageProvider.bucketName}:${media.relativeUri}`, { region: mediaStorageProvider.region });
76
- }
77
- }
62
+ await this.decorateMediaRecords(group.groupData.records);
78
63
  }
79
64
  }
80
65
  return data
81
66
  }
82
67
 
83
- async upload(createDto: any, files: Array<Express.Multer.File>) {
68
+ async findOne(id: number, query: any = {}, solidRequestContext: any = {}) {
69
+ const media = await super.findOne(id, query, solidRequestContext);
70
+ if (media) {
71
+ await this.decorateMediaRecord(media);
72
+ }
73
+ return media;
74
+ }
84
75
 
85
- if (!files) {
76
+ async upload(createDto: any, files: Array<Express.Multer.File>, _solidRequestContext: any = {}) {
77
+
78
+ if (!files || files.length === 0) {
86
79
  throw new NotFoundException(ERROR_MESSAGES.FILE_NOT_FOUND);
87
80
 
88
81
  }
89
- const savedMedias = [];
90
- for (let i = 0; i < files.length; i++) {
91
82
 
92
- createDto['fieldMetadata'] = await this.fieldMetadataRepo.findOne({
93
- where: {
94
- id: createDto['fieldMetadataId']
95
- },
96
- });
97
- createDto['modelMetadata'] = await this.modelMetadataRepo.findOne({
98
- where: {
99
- id: createDto['modelMetadataId']
100
- },
101
- });
102
- createDto['mediaStorageProviderMetadata'] = await this.mediaStorageProviderMetadataRepo.findOne({
103
- where: {
104
- id: createDto['mediaStorageProviderMetadataId']
105
- },
106
- });
83
+ createDto['fieldMetadata'] = await this.fieldMetadataRepo.findOne({
84
+ where: {
85
+ id: createDto['fieldMetadataId']
86
+ },
87
+ relations: ['mediaStorageProvider', 'model'],
88
+ });
89
+ createDto['modelMetadata'] = await this.modelMetadataRepo.findOne({
90
+ where: {
91
+ id: createDto['modelMetadataId']
92
+ },
93
+ });
94
+ createDto['mediaStorageProviderMetadata'] = await this.mediaStorageProviderMetadataRepo.findOne({
95
+ where: {
96
+ id: createDto['mediaStorageProviderMetadataId']
97
+ },
98
+ });
99
+ createDto['mediaStorageProviderMetadata'] = createDto['mediaStorageProviderMetadata'] || createDto['fieldMetadata']?.mediaStorageProvider;
100
+ createDto['modelMetadata'] = createDto['modelMetadata'] || createDto['fieldMetadata']?.model;
107
101
 
102
+ if (!createDto['mediaStorageProviderMetadata']) {
103
+ throw new NotFoundException('Media storage provider metadata not found');
104
+ }
105
+
106
+ const savedMedias = [];
107
+ for (let i = 0; i < files.length; i++) {
108
108
  const file = files[i];
109
+ const storageProvider = createDto.mediaStorageProviderMetadata as MediaStorageProviderMetadata;
110
+ const fileName = buildStoredMediaFileName(file);
109
111
 
110
- switch (createDto.mediaStorageProviderMetadata.type) {
112
+ switch (storageProvider.type) {
111
113
  case MediaStorageProviderType.Filesystem:
112
- const fileStoragePath = this.getFullFilePathForDisk(this.getFileName(file));
114
+ const fileStoragePath = buildDiskMediaPath(fileName, this.settingService, storageProvider);
113
115
  await this.diskFileService.copy(file.path, fileStoragePath);
114
- createDto['relativeUri'] = this.getFileName(file);
116
+ createDto['relativeUri'] = fileName;
115
117
  break;
116
118
  case MediaStorageProviderType.AwsS3:
117
- const fileName = this.getFileName(file);
118
- const bucketName = createDto.mediaStorageProviderMetadata.bucketName;
119
+ const bucketName = storageProvider.bucketName;
120
+ const region = getEffectiveS3Region(this.configService, storageProvider.region);
119
121
 
120
122
  // Read file from disk and upload to S3
121
123
  const fileData = await this.diskFileService.read(file.path);
122
- await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { contentType: file.mimetype });
124
+ await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { contentType: file.mimetype, region });
123
125
 
124
126
  createDto['relativeUri'] = fileName;
125
127
  break;
@@ -129,8 +131,10 @@ export class MediaService extends CRUDService<Media> {
129
131
  // Delete temp file from disk
130
132
  await this.diskFileService.delete(file.path);
131
133
 
132
- const media = this.repo.create(createDto);
134
+ createDto['isPublic'] = resolveStoredMediaIsPublic(createDto['isPublic'], storageProvider);
135
+ const media = this.repo.create(createDto as Partial<Media>) as Media;
133
136
  const savedMedia = await this.repo.save(media);
137
+ savedMedia.isPublic = resolveStoredMediaIsPublic(savedMedia.isPublic, storageProvider);
134
138
  savedMedias.push(savedMedia)
135
139
  }
136
140
  return savedMedias
@@ -157,17 +161,143 @@ export class MediaService extends CRUDService<Media> {
157
161
  return this.repo.remove(media);
158
162
  }
159
163
 
160
- private getFullFilePathForDisk(fileName: string): string {
161
- const base = this.settingService.getConfigValue<SolidCoreSetting>("fileStorageDir")
162
- || DEFAULT_MEDIA_FILE_STORAGE_DIR;
163
- if (path.isAbsolute(fileName) || fileName.startsWith(`${base}/`)) {
164
- return fileName;
164
+ async fileDownloadStream(media: Media): Promise<{ stream: Readable | null, fileName: string, mimeType: string, redirectUrl?: string }> {
165
+ const loadedMedia = await this.repo.findOne({
166
+ where: { id: media.id },
167
+ relations: ['mediaStorageProviderMetadata'],
168
+ });
169
+
170
+ if (!loadedMedia || !loadedMedia.mediaStorageProviderMetadata) {
171
+ throw new NotFoundException(`Media with id ${media.id} not found`);
172
+ }
173
+
174
+ const fileName = loadedMedia.originalFileName || path.basename(loadedMedia.relativeUri || `${loadedMedia.id}`);
175
+ const mimeType = loadedMedia.mimeType || 'application/octet-stream';
176
+
177
+ switch (loadedMedia.mediaStorageProviderMetadata.type as MediaStorageProviderType) {
178
+ case MediaStorageProviderType.Filesystem:
179
+ return {
180
+ stream: await this.diskFileService.readStream(
181
+ buildDiskMediaPath(loadedMedia.relativeUri, this.settingService, loadedMedia.mediaStorageProviderMetadata),
182
+ ),
183
+ fileName,
184
+ mimeType,
185
+ };
186
+ default:
187
+ throw new Error(`Unsupported media storage provider type ${loadedMedia.mediaStorageProviderMetadata.type}`);
165
188
  }
166
- return `${base}/${fileName}`;
167
189
  }
168
190
 
169
- private getFileName(file: Express.Multer.File): string {
170
- return `${file.filename}-${file.originalname}`;
191
+ async delete(id: number, solidRequestContext: any = {}) {
192
+ const media = await this.repo.findOne({
193
+ where: { id },
194
+ relations: ['mediaStorageProviderMetadata'],
195
+ });
196
+
197
+ const result = await super.delete(id, solidRequestContext);
198
+
199
+ if (media) {
200
+ await this.deletePhysicalFile(media);
201
+ }
202
+
203
+ return result;
171
204
  }
172
205
 
206
+ async deleteMany(ids: number[], solidRequestContext: any = {}) {
207
+ const mediaRecords = await this.repo.find({
208
+ where: {
209
+ id: In(ids),
210
+ },
211
+ relations: ['mediaStorageProviderMetadata'],
212
+ });
213
+
214
+ const result = await super.deleteMany(ids, solidRequestContext);
215
+
216
+ for (const media of mediaRecords) {
217
+ await this.deletePhysicalFile(media);
218
+ }
219
+
220
+ return result;
221
+ }
222
+
223
+ private async decorateMediaRecords(medias: Media[]): Promise<void> {
224
+ for (const media of medias) {
225
+ await this.decorateMediaRecord(media);
226
+ }
227
+ }
228
+
229
+ private async decorateMediaRecord(media: Media): Promise<void> {
230
+ const mediaStorageProvider = await this.resolveMediaStorageProvider(media);
231
+ media.isPublic = resolveStoredMediaIsPublic(media.isPublic, mediaStorageProvider);
232
+ media.relativeUri = await this.resolveMediaUrl(media, mediaStorageProvider);
233
+ }
234
+
235
+ private async resolveMediaUrl(media: Media, mediaStorageProvider?: MediaStorageProviderMetadata): Promise<string> {
236
+ const resolvedMediaStorageProvider = mediaStorageProvider || await this.resolveMediaStorageProvider(media);
237
+ const isPublic = resolveStoredMediaIsPublic(media.isPublic, resolvedMediaStorageProvider);
238
+
239
+ if (isPublic === false) {
240
+ return this.mediaDownloadUrlService.resolveDownloadUrl(media.id, media.relativeUri, resolvedMediaStorageProvider);
241
+ }
242
+
243
+ if (resolvedMediaStorageProvider?.type === MediaStorageProviderType.Filesystem) {
244
+ return this.diskFileService.getUrl(buildDiskMediaPath(media.relativeUri, this.settingService, resolvedMediaStorageProvider));
245
+ }
246
+
247
+ if (resolvedMediaStorageProvider?.type === MediaStorageProviderType.AwsS3) {
248
+ return this.s3FileService.getUrl(
249
+ `${resolvedMediaStorageProvider.bucketName}:${media.relativeUri}`,
250
+ { region: getEffectiveS3Region(this.configService, resolvedMediaStorageProvider.region), expiresIn: 0 },
251
+ );
252
+ }
253
+
254
+ return media.relativeUri;
255
+ }
256
+
257
+ private async deletePhysicalFile(media: Media): Promise<void> {
258
+ if (!media?.relativeUri) {
259
+ return;
260
+ }
261
+
262
+ try {
263
+ const mediaStorageProvider = await this.resolveMediaStorageProvider(media);
264
+ if (!mediaStorageProvider) {
265
+ return;
266
+ }
267
+
268
+ switch (mediaStorageProvider.type as MediaStorageProviderType) {
269
+ case MediaStorageProviderType.Filesystem:
270
+ await this.diskFileService.delete(buildDiskMediaPath(media.relativeUri, this.settingService, mediaStorageProvider));
271
+ return;
272
+ case MediaStorageProviderType.AwsS3:
273
+ await this.s3FileService.delete(
274
+ `${mediaStorageProvider.bucketName}:${media.relativeUri}`,
275
+ { region: getEffectiveS3Region(this.configService, mediaStorageProvider.region) },
276
+ );
277
+ return;
278
+ default:
279
+ this.logger.warn(`Skipping physical delete for unsupported media storage provider type ${mediaStorageProvider.type}`);
280
+ }
281
+ } catch (error: any) {
282
+ const message = error?.message ?? String(error);
283
+ this.logger.warn(`Failed to delete physical media file for media id ${media.id}: ${message}`);
284
+ }
285
+ }
286
+
287
+ private async resolveMediaStorageProvider(media: Media): Promise<MediaStorageProviderMetadata | undefined> {
288
+ if (media.mediaStorageProviderMetadata) {
289
+ return media.mediaStorageProviderMetadata;
290
+ }
291
+
292
+ if (!media.id) {
293
+ return undefined;
294
+ }
295
+
296
+ const loadedMedia = await this.repo.findOne({
297
+ where: { id: media.id },
298
+ relations: ['mediaStorageProviderMetadata'],
299
+ });
300
+
301
+ return loadedMedia?.mediaStorageProviderMetadata;
302
+ }
173
303
  }
@@ -9,6 +9,13 @@ import { MediaStorageProvider } from "src/interfaces";
9
9
  import { DiskFileService, S3FileService } from "src/services/file";
10
10
  import { Readable } from "stream";
11
11
  import { MediaRepository } from "src/repository/media.repository";
12
+ import { MediaDownloadUrlService } from "src/services/media-download-url.service";
13
+ import {
14
+ buildMediaRecordCreateInput,
15
+ buildStoredMediaFileName,
16
+ getEffectiveS3Region,
17
+ resolveStoredMediaIsPublic,
18
+ } from "src/services/media-storage.utils";
12
19
 
13
20
  @Injectable()
14
21
  export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
@@ -19,18 +26,14 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
19
26
  readonly diskFileService: DiskFileService,
20
27
  readonly s3FileService: S3FileService,
21
28
  readonly mediaRepository: MediaRepository,
29
+ private readonly mediaDownloadUrlService: MediaDownloadUrlService,
22
30
  ) { }
23
31
 
24
32
  async storeStreams(streamPairs: [Readable, string][], entity: T, mediaFieldMetadata: FieldMetadata): Promise<Media[]> {
25
- const isSupportedEntity = entity instanceof CommonEntity
26
- || entity instanceof LegacyCommonEntityWithExistingId
27
- || entity instanceof LegacyCommonEntityWithGeneratedId;
28
- if (!isSupportedEntity) {
29
- throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId");
30
- }
33
+ const entityId = this.getSupportedEntityId(entity);
31
34
  const result: Media[] = [];
32
35
  const storageProvider = mediaFieldMetadata.mediaStorageProvider;
33
- const region = this.getEffectiveRegion(storageProvider.region);
36
+ const region = getEffectiveS3Region(this.configService, storageProvider.region);
34
37
 
35
38
  for (const [stream, fileName] of streamPairs) {
36
39
  const bucketName = storageProvider.bucketName;
@@ -45,15 +48,13 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
45
48
 
46
49
  await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { region });
47
50
 
48
- const mediaEntity = await this.mediaRepository.createMedia({
49
- // @ts-ignore
50
- entityId: entity.id,
51
- modelMetadataId: mediaFieldMetadata.model.id,
52
- relativeUri: fileName,
53
- fileSize,
54
- mediaStorageProviderMetadataId: mediaFieldMetadata.mediaStorageProvider.id,
55
- fieldMetadataId: mediaFieldMetadata.id
56
- }) as unknown as Media;
51
+ const mediaEntity = await this.mediaRepository.createMedia(
52
+ buildMediaRecordCreateInput(entityId, mediaFieldMetadata, storageProvider, {
53
+ relativeUri: fileName,
54
+ fileSize,
55
+ })
56
+ ) as unknown as Media;
57
+ mediaEntity.isPublic = resolveStoredMediaIsPublic(mediaEntity.isPublic, storageProvider);
57
58
  result.push(mediaEntity);
58
59
  this.logger.debug(`Stored media with`, mediaEntity);
59
60
  }
@@ -61,29 +62,23 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
61
62
  }
62
63
 
63
64
  async retrieve(entity: T, mediaFieldMetadata: FieldMetadata): Promise<Media[]> {
64
- const isSupportedEntity = entity instanceof CommonEntity
65
- || entity instanceof LegacyCommonEntityWithExistingId
66
- || entity instanceof LegacyCommonEntityWithGeneratedId;
67
- if (!isSupportedEntity) {
68
- throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity
69
- }
65
+ const entityId = this.getSupportedEntityId(entity);
70
66
  // @ts-ignore
71
- const media = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entity.id, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']);
67
+ const media = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entityId, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']);
72
68
 
73
69
  // TODO: Check if the mediaStorageProvider (s3 in this case) is configured with a public bucket or not.
74
70
  // If private bucket then we need to return a "signed-url", the timeout for the signed url can be configured in the media storage provider entity and modified using the CRUD interface.
75
71
  // Add the full URL to the media
76
72
  for (const m of media) {
77
73
  const storageMeta = m.mediaStorageProviderMetadata;
78
- const region = this.getEffectiveRegion(storageMeta.region);
79
- if (storageMeta.isPublic === false) {
80
- // Generate signed URL
81
- const expiryInSeconds = (storageMeta.signedUrlExpiry ?? 60) * 60;
82
- m['_full_url'] = await this.s3FileService.getUrl(`${storageMeta?.bucketName}:${m.relativeUri}`, { expiresIn: expiryInSeconds, region });
83
- } else {
84
- // Public S3 or local filesystem: use normal URL
85
- m['_full_url'] = this.getFullFilePath(m);
86
- }
74
+ const isPublic = resolveStoredMediaIsPublic(m.isPublic, storageMeta);
75
+ m.isPublic = isPublic;
76
+ m['_full_url'] = isPublic === false
77
+ ? await this.mediaDownloadUrlService.resolveDownloadUrl(m.id, m.relativeUri, storageMeta)
78
+ : await this.s3FileService.getUrl(
79
+ `${storageMeta.bucketName}:${m.relativeUri}`,
80
+ { region: getEffectiveS3Region(this.configService, storageMeta.region), expiresIn: 0 },
81
+ );
87
82
  }
88
83
 
89
84
  return media;
@@ -94,18 +89,13 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
94
89
  }
95
90
 
96
91
  async store(files: Express.Multer.File[], entity: T, mediaFieldMetadata: FieldMetadata): Promise<Media[]> {
97
- const isSupportedEntity = entity instanceof CommonEntity
98
- || entity instanceof LegacyCommonEntityWithExistingId
99
- || entity instanceof LegacyCommonEntityWithGeneratedId;
100
- if (!isSupportedEntity) {
101
- throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity
102
- }
92
+ const entityId = this.getSupportedEntityId(entity);
103
93
  const result: Media[] = [];
104
94
  const storageProvider = mediaFieldMetadata.mediaStorageProvider;
105
- const region = this.getEffectiveRegion(storageProvider.region);
95
+ const region = getEffectiveS3Region(this.configService, storageProvider.region);
106
96
 
107
97
  for (const file of files) {
108
- const fileName = this.getFileName(file);
98
+ const fileName = buildStoredMediaFileName(file);
109
99
  const bucketName = storageProvider.bucketName;
110
100
 
111
101
  // Read file from disk and upload to S3
@@ -118,17 +108,15 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
118
108
  const awsFileUrl = fileName;
119
109
 
120
110
  // Create an entry in the media table
121
- const mediaEntity = await this.mediaRepository.createMedia({
122
- // @ts-ignore
123
- entityId: entity.id,
124
- modelMetadataId: mediaFieldMetadata.model.id,
125
- relativeUri: awsFileUrl,
126
- mimeType: file.mimetype,
127
- fileSize: file.size,
128
- originalFileName: file.originalname,
129
- mediaStorageProviderMetadataId: mediaFieldMetadata.mediaStorageProvider.id,
130
- fieldMetadataId: mediaFieldMetadata.id
131
- }) as unknown as Media;
111
+ const mediaEntity = await this.mediaRepository.createMedia(
112
+ buildMediaRecordCreateInput(entityId, mediaFieldMetadata, storageProvider, {
113
+ relativeUri: awsFileUrl,
114
+ mimeType: file.mimetype,
115
+ fileSize: file.size,
116
+ originalFileName: file.originalname,
117
+ })
118
+ ) as unknown as Media;
119
+ mediaEntity.isPublic = resolveStoredMediaIsPublic(mediaEntity.isPublic, storageProvider);
132
120
  result.push(mediaEntity);
133
121
  this.logger.debug(`Stored media with`, mediaEntity);
134
122
  };
@@ -136,19 +124,14 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
136
124
  }
137
125
 
138
126
  async delete(entity: T, mediaFieldMetadata: FieldMetadata): Promise<void> {
139
- const isSupportedEntity = entity instanceof CommonEntity
140
- || entity instanceof LegacyCommonEntityWithExistingId
141
- || entity instanceof LegacyCommonEntityWithGeneratedId;
142
- if (!isSupportedEntity) {
143
- throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity
144
- }
127
+ const entityId = this.getSupportedEntityId(entity);
145
128
  const storageProvider = mediaFieldMetadata.mediaStorageProvider;
146
- const region = this.getEffectiveRegion(storageProvider.region);
129
+ const region = getEffectiveS3Region(this.configService, storageProvider.region);
147
130
 
148
131
  // @ts-ignore
149
- const existingMedia = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entity.id, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']);
132
+ const existingMedia = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entityId, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']);
150
133
  // @ts-ignore
151
- this.mediaRepository.deleteByEntityIdAndFieldIdAndModelMetadataId(entity.id, mediaFieldMetadata.id, mediaFieldMetadata.model.id);
134
+ this.mediaRepository.deleteByEntityIdAndFieldIdAndModelMetadataId(entityId, mediaFieldMetadata.id, mediaFieldMetadata.model.id);
152
135
  for (const media of existingMedia) {
153
136
  const bucketName = storageProvider.bucketName;
154
137
  await this.s3FileService.delete(`${bucketName}:${media.relativeUri}`, { region });
@@ -163,26 +146,19 @@ export class FileS3StorageProvider<T> implements MediaStorageProvider<T> {
163
146
  if (!storageProvider?.bucketName || !media?.relativeUri) {
164
147
  return;
165
148
  }
166
- const region = this.getEffectiveRegion(storageProvider.region);
149
+ const region = getEffectiveS3Region(this.configService, storageProvider.region);
167
150
  await this.s3FileService.delete(`${storageProvider.bucketName}:${media.relativeUri}`, { region });
168
151
  }
169
152
 
170
- /**
171
- * Get the effective region to use for S3 operations.
172
- * Uses the provider-specific region if configured, otherwise falls back to env variable.
173
- */
174
- private getEffectiveRegion(providerRegion?: string): string | undefined {
175
- return providerRegion || this.configService.get('S3_AWS_REGION_NAME');
176
- }
153
+ private getSupportedEntityId(entity: T): number {
154
+ const isSupportedEntity = entity instanceof CommonEntity
155
+ || entity instanceof LegacyCommonEntityWithExistingId
156
+ || entity instanceof LegacyCommonEntityWithGeneratedId;
177
157
 
178
- private getFullFilePath(media: Media): string {
179
- // Use provider region if available, fallback to env variable
180
- const region = this.getEffectiveRegion(media.mediaStorageProviderMetadata.region);
181
- // https://lunarismedia.s3.ap-south-1.amazonaws.com/LUNARIS_CP_REGISTRATION_CREATIVE.jpg
182
- return `https://${media.mediaStorageProviderMetadata.bucketName}.s3.${region}.amazonaws.com/${media.relativeUri}`;
183
- }
158
+ if (!isSupportedEntity) {
159
+ throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId");
160
+ }
184
161
 
185
- private getFileName(file: Express.Multer.File): string {
186
- return `${file.filename}-${file.originalname}`;
162
+ return (entity as any).id;
187
163
  }
188
164
  }