@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,5 +1,5 @@
1
- import { IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
2
- import { ApiProperty } from '@nestjs/swagger';
1
+ import { IsBoolean, IsInt,IsOptional, IsNotEmpty, IsString } from 'class-validator';
2
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
3
3
  export class UpdateMediaDto {
4
4
  @IsOptional()
5
5
  @IsInt()
@@ -11,6 +11,11 @@ export class UpdateMediaDto {
11
11
  @ApiProperty()
12
12
  entityId: number;
13
13
 
14
+ @IsOptional()
15
+ @IsBoolean()
16
+ @ApiPropertyOptional({ default: true })
17
+ isPublic?: boolean;
18
+
14
19
  @IsOptional()
15
20
  @IsString()
16
21
  @ApiProperty()
@@ -60,4 +65,4 @@ fieldMetadataId: number;
60
65
  @IsOptional()
61
66
  @ApiProperty()
62
67
  fieldMetadataUserKey: string;
63
- }
68
+ }
@@ -9,6 +9,9 @@ export class Media extends CommonEntity {
9
9
  @Column({ type: "integer" })
10
10
  entityId: number;
11
11
 
12
+ @Column({ name: "is_public", default: true })
13
+ isPublic: boolean = true;
14
+
12
15
  @Column({ type: "varchar", nullable: true })
13
16
  relativeUri: string;
14
17
 
@@ -36,4 +39,4 @@ export class Media extends CommonEntity {
36
39
  @ManyToOne(() => FieldMetadata, { nullable: false })
37
40
  @JoinColumn()
38
41
  fieldMetadata: FieldMetadata;
39
- }
42
+ }
@@ -2,4 +2,5 @@ export enum AuthType {
2
2
  Bearer,
3
3
  ApiKey,
4
4
  None,
5
+ MediaSignedUrl,
5
6
  }
@@ -9,6 +9,7 @@ import { AUTH_TYPE_KEY } from '../decorators/auth.decorator';
9
9
  import { AuthType } from '../enums/auth-type.enum';
10
10
  import { AccessTokenGuard } from './access-token.guard';
11
11
  import { ApiKeyGuard } from './api-key.guard';
12
+ import { MediaSignedUrlGuard } from './media-signed-url.guard';
12
13
  import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
13
14
  import { PermissionMetadataService } from '../services/permission-metadata.service';
14
15
  import { ClsService } from 'nestjs-cls';
@@ -22,12 +23,14 @@ export class AuthenticationGuard implements CanActivate {
22
23
  [AuthType.Bearer]: this.accessTokenGuard,
23
24
  [AuthType.ApiKey]: this.apiKeyGuard,
24
25
  [AuthType.None]: { canActivate: () => true },
26
+ [AuthType.MediaSignedUrl]: this.mediaSignedUrlGuard,
25
27
  };
26
28
 
27
29
  constructor(
28
30
  private readonly reflector: Reflector,
29
31
  private readonly accessTokenGuard: AccessTokenGuard,
30
32
  private readonly apiKeyGuard: ApiKeyGuard,
33
+ private readonly mediaSignedUrlGuard: MediaSignedUrlGuard,
31
34
  private readonly permissionService: PermissionMetadataService,
32
35
  private readonly cls: ClsService,
33
36
  ) { }
@@ -0,0 +1,41 @@
1
+ import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
2
+ import { Request } from 'express';
3
+ import { MediaDownloadUrlService } from 'src/services/media-download-url.service';
4
+
5
+ @Injectable()
6
+ export class MediaSignedUrlGuard implements CanActivate {
7
+ constructor(private readonly mediaDownloadUrlService: MediaDownloadUrlService) { }
8
+
9
+ async canActivate(context: ExecutionContext): Promise<boolean> {
10
+ const request = context.switchToHttp().getRequest<Request>();
11
+ const token = this.extractTokenFromQuery(request);
12
+ if (!token) {
13
+ throw new UnauthorizedException();
14
+ }
15
+
16
+ let payload: { mediaId: number };
17
+ try {
18
+ payload = await this.mediaDownloadUrlService.verifyDownloadToken(token);
19
+ } catch {
20
+ throw new UnauthorizedException();
21
+ }
22
+
23
+ const routeMediaId = Number(request.params?.id);
24
+ if (!Number.isFinite(routeMediaId) || payload.mediaId !== routeMediaId) {
25
+ throw new UnauthorizedException();
26
+ }
27
+
28
+ // No REQUEST_USER_KEY is ever set for token auth (only Bearer/ApiKey guards set it), so
29
+ // the sibling global APP_GUARD PermissionsGuard would otherwise reject this request for
30
+ // lack of a per-user permission grant. Flag it the same way AuthenticationGuard flags
31
+ // Public-role-exempt requests - token possession IS the full authorization decision here.
32
+ request['isListedInPublicRole'] = true;
33
+
34
+ return true;
35
+ }
36
+
37
+ private extractTokenFromQuery(request: Request): string | undefined {
38
+ const token = request.query?.token;
39
+ return typeof token === 'string' ? token : undefined;
40
+ }
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { FieldCrudManager, ValidationError } from "src/interfaces";
2
+ import { DANGEROUS_EXTENSIONS, EXT_TO_MEDIA_TYPE, MediaCategory } from "src/constants/media-file-types";
2
3
 
3
4
  export enum SolidMediaType {
4
5
  mediaSingle = 'mediaSingle',
@@ -14,7 +15,7 @@ export interface MediaFieldOptions {
14
15
  isUpdate: boolean | undefined | null;
15
16
  }
16
17
 
17
- type MediaType = 'image' | 'audio' | 'video' | 'file' | 'pdf';
18
+ type MediaType = MediaCategory;
18
19
 
19
20
  const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
20
21
  // Images
@@ -25,7 +26,6 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
25
26
  'image/gif': 'image',
26
27
  'image/bmp': 'image',
27
28
  'image/tiff': 'image',
28
- 'image/svg+xml': 'image',
29
29
  'image/heic': 'image',
30
30
  'image/heif': 'image',
31
31
 
@@ -72,35 +72,23 @@ const MIME_TO_MEDIA_TYPE: Record<string, MediaType> = {
72
72
  'application/x-zip-compressed': 'file',
73
73
  'application/x-rar-compressed': 'file',
74
74
  'application/x-7z-compressed': 'file',
75
-
76
- // Common binary fallback category
77
- 'application/octet-stream': 'file',
78
75
  };
79
76
 
80
- const EXT_TO_MEDIA_TYPE: Record<string, MediaType> = {
81
- // Images
82
- png: 'image', jpg: 'image', jpeg: 'image', webp: 'image', gif: 'image', bmp: 'image', tiff: 'image', svg: 'image', heic: 'image', heif: 'image',
83
-
84
- // Audio
85
- mp3: 'audio', wav: 'audio', ogg: 'audio', aac: 'audio', m4a: 'audio', flac: 'audio',
86
-
87
- // Video
88
- mp4: 'video', mov: 'video', avi: 'video', mkv: 'video', mpeg: 'video', mpg: 'video', '3gp': 'video', '3g2': 'video',
89
-
90
- // Files
91
- pdf: 'file', txt: 'file', md: 'file', csv: 'file', json: 'file',
92
- doc: 'file', docx: 'file', xls: 'file', xlsx: 'file', ppt: 'file', pptx: 'file',
93
- zip: 'file', rar: 'file', '7z': 'file',
94
- };
95
-
96
-
97
77
  export class MediaFieldCrudManager implements FieldCrudManager {
98
78
 
99
79
  constructor(private readonly options: MediaFieldOptions) {
100
80
  }
101
81
 
102
82
  private resolveMediaType(mimetype?: string, filename?: string): MediaType | null {
83
+ const ext = (filename || '').split('.').pop()?.toLowerCase();
84
+ if (ext && DANGEROUS_EXTENSIONS.has(ext)) {
85
+ return null;
86
+ }
87
+
103
88
  const mt = (mimetype || '').toLowerCase().trim();
89
+ if (mt === 'image/svg+xml' || mt === 'text/html' || mt === 'application/xhtml+xml') {
90
+ return null;
91
+ }
104
92
  if (mt && MIME_TO_MEDIA_TYPE[mt]) {
105
93
  return MIME_TO_MEDIA_TYPE[mt];
106
94
  }
@@ -110,8 +98,8 @@ export class MediaFieldCrudManager implements FieldCrudManager {
110
98
  if (mt.startsWith('audio/')) return 'audio';
111
99
  if (mt.startsWith('video/')) return 'video';
112
100
 
113
- // Fallback to extension if provided
114
- const ext = (filename || '').split('.').pop()?.toLowerCase();
101
+ // Fallback to extension if provided (also covers generic/unrecognized
102
+ // mimetypes such as application/octet-stream).
115
103
  if (ext && EXT_TO_MEDIA_TYPE[ext]) {
116
104
  return EXT_TO_MEDIA_TYPE[ext];
117
105
  }
@@ -7,6 +7,7 @@ import { MediaStorageProviderMetadataRepository } from './media-storage-provider
7
7
  import { ModelMetadataRepository } from './model-metadata.repository';
8
8
  import { SecurityRuleRepository } from './security-rule.repository';
9
9
  import { SolidBaseRepository } from './solid-base.repository';
10
+ import { resolveStoredMediaIsPublic } from 'src/services/media-storage.utils';
10
11
 
11
12
  @Injectable()
12
13
  export class MediaRepository extends SolidBaseRepository<Media> {
@@ -44,6 +45,7 @@ export class MediaRepository extends SolidBaseRepository<Media> {
44
45
  id: createDto['mediaStorageProviderMetadataId']
45
46
  },
46
47
  });
48
+ createDto['isPublic'] = resolveStoredMediaIsPublic(createDto['isPublic'], createDto['mediaStorageProviderMetadata']);
47
49
  const media = this.create(createDto);
48
50
  return this.save(media);
49
51
  }
@@ -1704,6 +1704,18 @@
1704
1704
  "columnName": "original_file_name",
1705
1705
  "isSystem": true
1706
1706
  },
1707
+ {
1708
+ "name": "isPublic",
1709
+ "displayName": "Is Public",
1710
+ "type": "boolean",
1711
+ "required": false,
1712
+ "unique": false,
1713
+ "index": false,
1714
+ "private": false,
1715
+ "encrypt": false,
1716
+ "columnName": "is_public",
1717
+ "isSystem": true
1718
+ },
1707
1719
  {
1708
1720
  "name": "modelMetadata",
1709
1721
  "displayName": "Model Metadata",
@@ -5306,6 +5318,9 @@
5306
5318
  "FieldMetadataController.getSelectionDynamicValues",
5307
5319
  "FieldMetadataController.getSelectionDynamicValue",
5308
5320
  "FieldMetadataController.findFieldDefaultMetaData",
5321
+ "MediaController.findMany",
5322
+ "MediaController.findOne",
5323
+ "MediaController.download",
5309
5324
  "SavedFiltersController.delete",
5310
5325
  "SavedFiltersController.deleteMany",
5311
5326
  "SavedFiltersController.findOne",
@@ -8607,9 +8622,9 @@
8607
8622
  "attrs": {
8608
8623
  "name": "roles",
8609
8624
  "widget": "checkbox",
8610
- "inlineCreateAutoSave": "true",
8625
+ "inlineCreateAutoSave": "false",
8611
8626
  "renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor",
8612
- "inlineCreate": "true",
8627
+ "inlineCreate": "false",
8613
8628
  "inlineCreateLayout": {
8614
8629
  "type": "form",
8615
8630
  "attrs": {
@@ -8758,6 +8773,13 @@
8758
8773
  "sortable": false
8759
8774
  }
8760
8775
  },
8776
+ {
8777
+ "type": "field",
8778
+ "attrs": {
8779
+ "name": "isPublic",
8780
+ "sortable": true
8781
+ }
8782
+ },
8761
8783
  {
8762
8784
  "type": "field",
8763
8785
  "attrs": {
@@ -8853,6 +8875,14 @@
8853
8875
  "isSearchable": true
8854
8876
  }
8855
8877
  },
8878
+ {
8879
+ "type": "field",
8880
+ "attrs": {
8881
+ "name": "isPublic",
8882
+ "label": "isPublic",
8883
+ "isSearchable": true
8884
+ }
8885
+ },
8856
8886
  {
8857
8887
  "type": "field",
8858
8888
  "attrs": {
@@ -8921,6 +8951,12 @@
8921
8951
  "name": "mediaStorageProviderMetadata"
8922
8952
  }
8923
8953
  },
8954
+ {
8955
+ "type": "field",
8956
+ "attrs": {
8957
+ "name": "isPublic"
8958
+ }
8959
+ },
8924
8960
  {
8925
8961
  "type": "field",
8926
8962
  "attrs": {
@@ -14525,7 +14561,15 @@
14525
14561
  "mediaStorageProviders": [
14526
14562
  {
14527
14563
  "name": "default-filesystem",
14528
- "type": "filesystem"
14564
+ "type": "filesystem",
14565
+ "isPublic": true,
14566
+ "localPath": "media-files-storage"
14567
+ },
14568
+ {
14569
+ "name": "private-filesystem",
14570
+ "type": "filesystem",
14571
+ "isPublic": false,
14572
+ "localPath": "media-private-files-storage"
14529
14573
  },
14530
14574
  {
14531
14575
  "name": "default-aws-s3",
@@ -14561,6 +14605,47 @@
14561
14605
  }
14562
14606
  }
14563
14607
  }
14608
+ },
14609
+ {
14610
+ "name": "model:Media-role:Internal User-own",
14611
+ "description": "Show Media records owned by the current user",
14612
+ "roleUserKey": "Internal User",
14613
+ "modelMetadataUserKey": "media",
14614
+ "securityRuleConfig": {
14615
+ "filters": {
14616
+ "createdBy": {
14617
+ "$eq": "$activeUserId"
14618
+ }
14619
+ }
14620
+ }
14621
+ },
14622
+ {
14623
+ "name": "model:Media-role:Internal User-public",
14624
+ "description": "Show public Media records",
14625
+ "roleUserKey": "Internal User",
14626
+ "modelMetadataUserKey": "media",
14627
+ "securityRuleConfig": {
14628
+ "filters": {
14629
+ "mediaStorageProviderMetadata": {
14630
+ "isPublic": {
14631
+ "$eq": true
14632
+ }
14633
+ }
14634
+ }
14635
+ }
14636
+ },
14637
+ {
14638
+ "name": "model:Media-role:Admin",
14639
+ "description": "Show All Media records",
14640
+ "roleUserKey": "Admin",
14641
+ "modelMetadataUserKey": "media",
14642
+ "securityRuleConfig": {
14643
+ "filters": {
14644
+ "id": {
14645
+ "$ne": "0"
14646
+ }
14647
+ }
14648
+ }
14564
14649
  }
14565
14650
  ],
14566
14651
  "modelSequences": [],
@@ -0,0 +1,6 @@
1
+ 1. Do i need to create a storeStreams method for aws service too?
2
+ - Handle later
3
+ 2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
4
+ - startExport should either return the data or return the transaction id
5
+ 3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
6
+ - show the userKey
@@ -0,0 +1,67 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { JwtService } from '@nestjs/jwt';
4
+ import { MediaStorageProviderType } from 'src/dtos/create-media-storage-provider-metadata.dto';
5
+ import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
6
+ import { S3FileService } from 'src/services/file';
7
+ import { getEffectiveS3Region } from 'src/services/media-storage.utils';
8
+ import { SettingService } from 'src/services/setting.service';
9
+ import type { SolidCoreSetting } from 'src/services/settings/default-settings-provider.service';
10
+
11
+ export const MEDIA_DOWNLOAD_TOKEN_AUDIENCE = 'media-download';
12
+ const DEFAULT_SIGNED_URL_EXPIRY_MINUTES = 60;
13
+
14
+ export interface MediaDownloadTokenPayload {
15
+ mediaId: number;
16
+ }
17
+
18
+ @Injectable()
19
+ export class MediaDownloadUrlService {
20
+ private readonly logger = new Logger(MediaDownloadUrlService.name);
21
+
22
+ constructor(
23
+ private readonly jwtService: JwtService,
24
+ private readonly s3FileService: S3FileService,
25
+ private readonly settingService: SettingService,
26
+ private readonly configService: ConfigService,
27
+ ) { }
28
+
29
+ async resolveDownloadUrl(mediaId: number, relativeUri: string, storageProvider?: MediaStorageProviderMetadata): Promise<string> {
30
+ const expiryMinutes = storageProvider?.signedUrlExpiry ?? DEFAULT_SIGNED_URL_EXPIRY_MINUTES;
31
+
32
+ if ((storageProvider?.type as MediaStorageProviderType) === MediaStorageProviderType.AwsS3) {
33
+ if (storageProvider?.bucketName) {
34
+ return this.s3FileService.getUrl(
35
+ `${storageProvider.bucketName}:${relativeUri}`,
36
+ { region: getEffectiveS3Region(this.configService, storageProvider.region), expiresIn: expiryMinutes * 60 },
37
+ );
38
+ }
39
+ this.logger.warn(`Media id ${mediaId}: AwsS3 provider missing bucketName; falling back to app-hosted token URL`);
40
+ }
41
+
42
+ // Filesystem (or misconfigured S3) has no native presigned-URL mechanism, so mint our
43
+ // own short-lived token and embed it in the download URL - the token alone authorizes
44
+ // the request (see MediaSignedUrlGuard), so it works in a plain <img src> with no
45
+ // Authorization header needed.
46
+ const token = await this.mintDownloadToken(mediaId, expiryMinutes);
47
+ const downloadPath = `/api/media/${mediaId}/download?token=${token}`;
48
+ const baseUrl = this.settingService.getConfigValue<SolidCoreSetting>('baseUrl');
49
+ return `${String(baseUrl).replace(/\/+$/, '')}${downloadPath}`;
50
+ }
51
+
52
+ async verifyDownloadToken(token: string): Promise<MediaDownloadTokenPayload> {
53
+ return this.jwtService.verifyAsync<MediaDownloadTokenPayload>(token, {
54
+ secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
55
+ audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
56
+ });
57
+ }
58
+
59
+ private mintDownloadToken(mediaId: number, expiryMinutes: number): Promise<string> {
60
+ const payload: MediaDownloadTokenPayload = { mediaId };
61
+ return this.jwtService.signAsync(payload, {
62
+ secret: this.settingService.getConfigValue<SolidCoreSetting>('secret'),
63
+ audience: MEDIA_DOWNLOAD_TOKEN_AUDIENCE,
64
+ expiresIn: expiryMinutes * 60,
65
+ });
66
+ }
67
+ }
@@ -0,0 +1,79 @@
1
+ import { ConfigService } from '@nestjs/config';
2
+ import * as path from 'path';
3
+ import { FieldMetadata } from 'src/entities/field-metadata.entity';
4
+ import { MediaStorageProviderMetadata } from 'src/entities/media-storage-provider-metadata.entity';
5
+ import { SettingService } from 'src/services/setting.service';
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';
8
+
9
+ export const DEFAULT_PRIVATE_MEDIA_STORAGE_DIR = 'media-private-files-storage';
10
+
11
+ export interface MediaRecordCreateInput {
12
+ entityId: number;
13
+ modelMetadataId: number;
14
+ mediaStorageProviderMetadataId: number;
15
+ fieldMetadataId: number;
16
+ relativeUri: string;
17
+ mimeType?: string;
18
+ fileSize?: number;
19
+ originalFileName?: string;
20
+ isPublic?: boolean;
21
+ }
22
+
23
+ // Builds the stored file name format shared by media storage implementations.
24
+ export function buildStoredMediaFileName(file: Pick<Express.Multer.File, 'filename' | 'originalname'>): string {
25
+ return `${file.filename}-${file.originalname}`;
26
+ }
27
+
28
+ // Resolves the on-disk path for a media file based on settings and provider visibility.
29
+ export function buildDiskMediaPath(fileName: string,settingService: Pick<SettingService, 'getConfigValue'>,storageProvider?: Pick<MediaStorageProviderMetadata, 'localPath' | 'isPublic'>,): string {
30
+ const publicBase = settingService.getConfigValue<SolidCoreSetting>('fileStorageDir') || DEFAULT_MEDIA_FILE_STORAGE_DIR;
31
+ const privateBase = DEFAULT_PRIVATE_MEDIA_STORAGE_DIR;
32
+ const providerBase = storageProvider?.localPath || (storageProvider?.isPublic === false ? privateBase : publicBase);
33
+
34
+ if ( path.isAbsolute(fileName) || fileName.startsWith(`${publicBase}/`) || fileName.startsWith(`${privateBase}/`) || (!!storageProvider?.localPath && fileName.startsWith(`${storageProvider.localPath}/`))) {
35
+ return fileName;
36
+ }
37
+
38
+ return `${providerBase}/${fileName}`;
39
+ }
40
+
41
+ // Returns the S3 region from the provider when present, otherwise falls back to app config.
42
+ export function getEffectiveS3Region(configService: Pick<ConfigService, 'get'>,providerRegion?: string,): string | undefined {
43
+ return providerRegion || configService.get<string>('S3_AWS_REGION_NAME');
44
+ }
45
+
46
+ // Resolves the effective public/private flag for a media storage provider.
47
+ export function resolveMediaIsPublic(storageProvider?: Pick<MediaStorageProviderMetadata, 'isPublic'>,): boolean | undefined {
48
+ if (!storageProvider) {
49
+ return undefined;
50
+ }
51
+
52
+ return storageProvider.isPublic !== false;
53
+ }
54
+
55
+ // Preserves an explicit media visibility value or derives it from the storage provider.
56
+ export function resolveStoredMediaIsPublic(currentValue: boolean | undefined,storageProvider?:Pick<MediaStorageProviderMetadata, 'isPublic'>,): boolean | undefined {
57
+ if (typeof currentValue === 'boolean') {
58
+ return currentValue;
59
+ }
60
+
61
+ return resolveMediaIsPublic(storageProvider);
62
+ }
63
+
64
+ // Builds the shared media record payload used before saving media metadata to the database.
65
+ export function buildMediaRecordCreateInput(
66
+ entityId: number,
67
+ mediaFieldMetadata: Pick<FieldMetadata, 'id' | 'model'> & { model: { id: number } },
68
+ storageProvider: Pick<MediaStorageProviderMetadata, 'id'>,
69
+ overrides: Pick<MediaRecordCreateInput, 'relativeUri'>
70
+ & Partial<Omit<MediaRecordCreateInput, 'entityId' | 'modelMetadataId' | 'mediaStorageProviderMetadataId' | 'fieldMetadataId' | 'relativeUri'>>,
71
+ ): MediaRecordCreateInput {
72
+ return {
73
+ entityId,
74
+ modelMetadataId: mediaFieldMetadata.model.id,
75
+ mediaStorageProviderMetadataId: storageProvider.id,
76
+ fieldMetadataId: mediaFieldMetadata.id,
77
+ ...overrides,
78
+ };
79
+ }