@solidxai/core 0.1.12-beta.2 → 0.1.12-beta.4
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/.claude/settings.local.json +16 -0
- package/CLAUDE.md +26 -0
- package/dist/commands/seed.command.d.ts.map +1 -1
- package/dist/commands/seed.command.js +15 -2
- package/dist/commands/seed.command.js.map +1 -1
- package/dist/constants/media-file-types.d.ts +5 -0
- package/dist/constants/media-file-types.d.ts.map +1 -0
- package/dist/constants/media-file-types.js +27 -0
- package/dist/constants/media-file-types.js.map +1 -0
- package/dist/controllers/media.controller.d.ts +14 -11
- package/dist/controllers/media.controller.d.ts.map +1 -1
- package/dist/controllers/media.controller.js +72 -33
- package/dist/controllers/media.controller.js.map +1 -1
- package/dist/dtos/create-media.dto.d.ts +1 -0
- package/dist/dtos/create-media.dto.d.ts.map +1 -1
- package/dist/dtos/create-media.dto.js +7 -1
- package/dist/dtos/create-media.dto.js.map +1 -1
- package/dist/dtos/create-role-metadata.dto.d.ts +2 -0
- package/dist/dtos/create-role-metadata.dto.d.ts.map +1 -1
- package/dist/dtos/create-role-metadata.dto.js +3 -1
- package/dist/dtos/create-role-metadata.dto.js.map +1 -1
- package/dist/dtos/update-media.dto.d.ts +1 -0
- package/dist/dtos/update-media.dto.d.ts.map +1 -1
- package/dist/dtos/update-media.dto.js +7 -1
- package/dist/dtos/update-media.dto.js.map +1 -1
- package/dist/entities/media.entity.d.ts +1 -0
- package/dist/entities/media.entity.d.ts.map +1 -1
- package/dist/entities/media.entity.js +9 -1
- package/dist/entities/media.entity.js.map +1 -1
- package/dist/enums/auth-type.enum.d.ts +2 -1
- package/dist/enums/auth-type.enum.d.ts.map +1 -1
- package/dist/enums/auth-type.enum.js +1 -0
- package/dist/enums/auth-type.enum.js.map +1 -1
- package/dist/guards/authentication.guard.d.ts +3 -1
- package/dist/guards/authentication.guard.d.ts.map +1 -1
- package/dist/guards/authentication.guard.js +5 -1
- package/dist/guards/authentication.guard.js.map +1 -1
- package/dist/guards/media-signed-url.guard.d.ts +9 -0
- package/dist/guards/media-signed-url.guard.d.ts.map +1 -0
- package/dist/guards/media-signed-url.guard.js +49 -0
- package/dist/guards/media-signed-url.guard.js.map +1 -0
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.d.ts.map +1 -1
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js +10 -13
- package/dist/helpers/field-crud-managers/MediaFieldCrudManager.js.map +1 -1
- package/dist/interfaces.d.ts +19 -1
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -1
- package/dist/repository/media.repository.d.ts.map +1 -1
- package/dist/repository/media.repository.js +2 -0
- package/dist/repository/media.repository.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts +7 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +87 -3
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.d.ts +1 -0
- package/dist/seeders/permission-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/permission-metadata-seeder.service.js +6 -1
- package/dist/seeders/permission-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +114 -13
- package/dist/services/authentication.service.d.ts +1 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +19 -1
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/media-download-url.service.d.ts +21 -0
- package/dist/services/media-download-url.service.d.ts.map +1 -0
- package/dist/services/media-download-url.service.js +67 -0
- package/dist/services/media-download-url.service.js.map +1 -0
- package/dist/services/media-storage.utils.d.ts +27 -0
- package/dist/services/media-storage.utils.d.ts.map +1 -0
- package/dist/services/media-storage.utils.js +82 -0
- package/dist/services/media-storage.utils.js.map +1 -0
- package/dist/services/media.service.d.ts +20 -4
- package/dist/services/media.service.d.ts.map +1 -1
- package/dist/services/media.service.js +157 -55
- package/dist/services/media.service.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts +4 -4
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js +37 -63
- package/dist/services/mediaStorageProviders/file-s3-storage-provider.js.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts +5 -5
- package/dist/services/mediaStorageProviders/file-storage-provider.d.ts.map +1 -1
- package/dist/services/mediaStorageProviders/file-storage-provider.js +37 -38
- package/dist/services/mediaStorageProviders/file-storage-provider.js.map +1 -1
- package/dist/services/role-metadata.service.d.ts +1 -0
- package/dist/services/role-metadata.service.d.ts.map +1 -1
- package/dist/services/role-metadata.service.js +8 -5
- package/dist/services/role-metadata.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +11 -1
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.js +1 -1
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/seed.command.ts +17 -3
- package/src/constants/media-file-types.ts +44 -0
- package/src/controllers/media.controller.ts +44 -24
- package/src/dtos/create-media.dto.ts +8 -3
- package/src/dtos/create-role-metadata.dto.ts +2 -0
- package/src/dtos/update-media.dto.ts +8 -3
- package/src/entities/media.entity.ts +4 -1
- package/src/enums/auth-type.enum.ts +1 -0
- package/src/guards/authentication.guard.ts +3 -0
- package/src/guards/media-signed-url.guard.ts +41 -0
- package/src/helpers/field-crud-managers/MediaFieldCrudManager.ts +12 -24
- package/src/interfaces.ts +20 -0
- package/src/repository/media.repository.ts +2 -0
- package/src/seeders/module-metadata-seeder.service.ts +109 -3
- package/src/seeders/permission-metadata-seeder.service.ts +6 -1
- package/src/seeders/seed-data/solid-core-metadata.json +114 -13
- package/src/services/1.js +6 -0
- package/src/services/authentication.service.ts +24 -1
- package/src/services/media-download-url.service.ts +67 -0
- package/src/services/media-storage.utils.ts +79 -0
- package/src/services/media.service.ts +189 -59
- package/src/services/mediaStorageProviders/file-s3-storage-provider.ts +52 -76
- package/src/services/mediaStorageProviders/file-storage-provider.ts +44 -48
- package/src/services/role-metadata.service.ts +17 -5
- package/src/solid-core.module.ts +21 -2
- package/src/subscribers/audit.subscriber.ts +1 -1
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
- package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +0 -136
- package/docs/agent-builder/team-ready-note-agent-builder.md +0 -305
- package/docs/agent-hub-grooming.md +0 -301
- package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +0 -438
- package/docs/dashboards/dashboard-curl-smoke-tests.txt +0 -146
- package/docs/dashboards/delete-legacy-dashboard-metadata.sql +0 -172
- package/docs/datasource-introspection-ddl-analysis.md +0 -326
- package/docs/datasource-introspection-implementation-plan.md +0 -306
- package/docs/grouping-enhancements.md +0 -89
- package/docs/java-spring/README.md +0 -3
- package/docs/java-spring/solid-core-module-deep-dive-report.md +0 -1317
- package/docs/module-package-import-handoff.md +0 -691
- package/docs/seed-changes.md +0 -65
- package/docs/test-data-workflow.md +0 -200
- package/docs/type-declaration-import-issue.md +0 -24
- package/docs/workflow/business-automation-example-notes.md +0 -309
- package/docs/workflow/data-engineering-pipeline-example-notes.md +0 -330
- package/docs/workflow/foreach-example-notes.md +0 -187
- package/docs/workflow/hello-world-example-notes.md +0 -271
- package/docs/workflow/kestra-concepts-plugins-blueprints.md +0 -315
- package/docs/workflow/loop-until-example-notes.md +0 -198
- package/docs/workflow/microservices-and-apis-example-notes.md +0 -264
- package/docs/workflow/samples.md +0 -25
- package/docs/workflow/what-is-kestra.md +0 -79
- package/docs/workflow/workflow-core-module-handoff-summary.md +0 -191
- package/docs/workflow/workflow-module-brd.md +0 -185
- package/docs/workflow/workflow-module-domain-model.md +0 -252
- package/docs/workflow/workflow-module-metadata-dsl.md +0 -302
- package/docs/workflow/workflow-module-technical-spec.md +0 -293
- package/docs/workflow/workflow-node-type-contracts.md +0 -635
- package/docs/workflow/workflow-node-ui-metadata-contract.md +0 -1262
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 (
|
|
112
|
+
switch (storageProvider.type) {
|
|
111
113
|
case MediaStorageProviderType.Filesystem:
|
|
112
|
-
const fileStoragePath =
|
|
114
|
+
const fileStoragePath = buildDiskMediaPath(fileName, this.settingService, storageProvider);
|
|
113
115
|
await this.diskFileService.copy(file.path, fileStoragePath);
|
|
114
|
-
createDto['relativeUri'] =
|
|
116
|
+
createDto['relativeUri'] = fileName;
|
|
115
117
|
break;
|
|
116
118
|
case MediaStorageProviderType.AwsS3:
|
|
117
|
-
const
|
|
118
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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(
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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.
|
|
95
|
+
const region = getEffectiveS3Region(this.configService, storageProvider.region);
|
|
106
96
|
|
|
107
97
|
for (const file of files) {
|
|
108
|
-
const fileName =
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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.
|
|
129
|
+
const region = getEffectiveS3Region(this.configService, storageProvider.region);
|
|
147
130
|
|
|
148
131
|
// @ts-ignore
|
|
149
|
-
const existingMedia = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(
|
|
132
|
+
const existingMedia = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entityId, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']);
|
|
150
133
|
// @ts-ignore
|
|
151
|
-
this.mediaRepository.deleteByEntityIdAndFieldIdAndModelMetadataId(
|
|
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.
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
return `${file.filename}-${file.originalname}`;
|
|
162
|
+
return (entity as any).id;
|
|
187
163
|
}
|
|
188
164
|
}
|