@solidstarters/solid-core 1.2.63 → 1.2.66

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 (70) hide show
  1. package/dist/controllers/chatter-message.controller.d.ts +1 -2
  2. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  3. package/dist/controllers/chatter-message.controller.js +3 -7
  4. package/dist/controllers/chatter-message.controller.js.map +1 -1
  5. package/dist/decorators/active-user.decorator.d.ts.map +1 -1
  6. package/dist/decorators/active-user.decorator.js.map +1 -1
  7. package/dist/decorators/solid-request-context.decorator.d.ts.map +1 -1
  8. package/dist/decorators/solid-request-context.decorator.js.map +1 -1
  9. package/dist/guards/access-token.guard.d.ts +3 -1
  10. package/dist/guards/access-token.guard.d.ts.map +1 -1
  11. package/dist/guards/access-token.guard.js +6 -2
  12. package/dist/guards/access-token.guard.js.map +1 -1
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +0 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/interfaces.d.ts +3 -0
  18. package/dist/interfaces.d.ts.map +1 -1
  19. package/dist/services/chatter-message-details.service.d.ts +1 -3
  20. package/dist/services/chatter-message-details.service.d.ts.map +1 -1
  21. package/dist/services/chatter-message-details.service.js +3 -6
  22. package/dist/services/chatter-message-details.service.js.map +1 -1
  23. package/dist/services/chatter-message.service.d.ts +7 -8
  24. package/dist/services/chatter-message.service.d.ts.map +1 -1
  25. package/dist/services/chatter-message.service.js +28 -19
  26. package/dist/services/chatter-message.service.js.map +1 -1
  27. package/dist/services/crud-helper.service.d.ts +1 -0
  28. package/dist/services/crud-helper.service.d.ts.map +1 -1
  29. package/dist/services/crud-helper.service.js +14 -1
  30. package/dist/services/crud-helper.service.js.map +1 -1
  31. package/dist/services/crud.service.d.ts +7 -3
  32. package/dist/services/crud.service.d.ts.map +1 -1
  33. package/dist/services/crud.service.js +91 -88
  34. package/dist/services/crud.service.js.map +1 -1
  35. package/dist/services/field-metadata.service.d.ts.map +1 -1
  36. package/dist/services/field-metadata.service.js +23 -11
  37. package/dist/services/field-metadata.service.js.map +1 -1
  38. package/dist/services/request-context.service.d.ts +4 -4
  39. package/dist/services/request-context.service.d.ts.map +1 -1
  40. package/dist/services/request-context.service.js +6 -10
  41. package/dist/services/request-context.service.js.map +1 -1
  42. package/dist/solid-core.module.d.ts.map +1 -1
  43. package/dist/solid-core.module.js +6 -2
  44. package/dist/solid-core.module.js.map +1 -1
  45. package/dist/subscribers/audit.subscriber.d.ts +1 -3
  46. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  47. package/dist/subscribers/audit.subscriber.js +7 -12
  48. package/dist/subscribers/audit.subscriber.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +2 -1
  51. package/src/controllers/chatter-message.controller.ts +1 -2
  52. package/src/decorators/active-user.decorator.ts +1 -0
  53. package/src/decorators/solid-request-context.decorator.ts +2 -1
  54. package/src/guards/access-token.guard.ts +3 -0
  55. package/src/index.ts +0 -1
  56. package/src/interfaces.ts +5 -1
  57. package/src/services/chatter-message-details.service.ts +1 -3
  58. package/src/services/chatter-message.service.ts +29 -20
  59. package/src/services/crud-helper.service.ts +19 -1
  60. package/src/services/crud.service.ts +110 -64
  61. package/src/services/field-metadata.service.ts +23 -12
  62. package/src/services/request-context.service.ts +5 -6
  63. package/src/solid-core.module.ts +5 -2
  64. package/src/subscribers/audit.subscriber.ts +5 -9
  65. package/dist/services/user-context.service.d.ts +0 -10
  66. package/dist/services/user-context.service.d.ts.map +0 -1
  67. package/dist/services/user-context.service.js +0 -42
  68. package/dist/services/user-context.service.js.map +0 -1
  69. package/src/services/1.js +0 -6
  70. package/src/services/user-context.service.ts +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidstarters/solid-core",
3
- "version": "1.2.63",
3
+ "version": "1.2.66",
4
4
  "description": "This module is a NestJS module containing all the required core providers required by a Solid application",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -51,6 +51,7 @@
51
51
  "mailgen": "^2.0.28",
52
52
  "mongoose": "^8.7.0",
53
53
  "mysql2": "^3.13.0",
54
+ "nestjs-cls": "^5.4.3",
54
55
  "nodemailer": "^6.9.13",
55
56
  "passport": "^0.7.0",
56
57
  "passport-google-oauth2": "^0.2.0",
@@ -98,8 +98,7 @@ export class ChatterMessageController {
98
98
  async postMessage(
99
99
  @Body() postDto: PostChatterMessageDto,
100
100
  @UploadedFiles() files: Array<Express.Multer.File>,
101
- @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto
102
101
  ) {
103
- return this.service.postMessage(postDto, solidRequestContext, files);
102
+ return this.service.postMessage(postDto, files);
104
103
  }
105
104
  }
@@ -5,6 +5,7 @@ import { REQUEST_USER_KEY } from "../constants";
5
5
  export const ActiveUser = createParamDecorator(
6
6
  (field: keyof ActiveUserData | undefined, ctx: ExecutionContext) => {
7
7
  const request = ctx.switchToHttp().getRequest();
8
+ // TODO idealiy we should use RequestContextService and getActiveUser method. (Works for async scenarios too)
8
9
  const user: ActiveUserData | undefined = request[REQUEST_USER_KEY];
9
10
  return field ? user?.[field] : user;
10
11
  },
@@ -6,7 +6,8 @@ import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto';
6
6
  export const SolidRequestContextDecorator = createParamDecorator(
7
7
  (data: unknown, ctx: ExecutionContext): SolidRequestContextDto => {
8
8
  const request = ctx.switchToHttp().getRequest();
9
- const activeUser: ActiveUserData | undefined = request[REQUEST_USER_KEY]; // Get active user
9
+ // TODO idealiy we should use RequestContextService and getActiveUser method. (Works for async scenarios too)
10
+ const activeUser: ActiveUserData | undefined = request[REQUEST_USER_KEY];
10
11
 
11
12
  // Create a new instance of SolidRequestContextDto and stamp user data
12
13
  const solidRequestContext = new SolidRequestContextDto();
@@ -12,6 +12,7 @@ import { ActiveUserData } from '../interfaces/active-user-data.interface';
12
12
  import { jwtConfig } from '../config/iam.config';
13
13
  import { REQUEST_USER_KEY } from "../constants";
14
14
  import { PermissionMetadataService } from '../services/permission-metadata.service';
15
+ import { ClsService } from 'nestjs-cls';
15
16
 
16
17
  @Injectable()
17
18
  export class AccessTokenGuard implements CanActivate {
@@ -20,6 +21,7 @@ export class AccessTokenGuard implements CanActivate {
20
21
  @Inject(jwtConfig.KEY)
21
22
  private readonly jwtConfiguration: ConfigType<typeof jwtConfig>,
22
23
  private readonly permissionsService: PermissionMetadataService,
24
+ private readonly cls: ClsService
23
25
  ) { }
24
26
 
25
27
  async canActivate(context: ExecutionContext): Promise<boolean> {
@@ -40,6 +42,7 @@ export class AccessTokenGuard implements CanActivate {
40
42
  payload.permissions = permissions.map((permission) => permission.name);
41
43
 
42
44
  request[REQUEST_USER_KEY] = payload;
45
+ this.cls.set(REQUEST_USER_KEY, payload);
43
46
  // console.log(`About to set payload in the request user key:`);
44
47
  // console.log(payload);
45
48
  } catch {
package/src/index.ts CHANGED
@@ -228,7 +228,6 @@ export * from './services/security-rule.service'
228
228
  export * from './services/request-context.service'
229
229
  export * from './services/chatter-message.service'
230
230
  export * from './services/chatter-message-details.service'
231
- export * from './services/user-context.service'
232
231
  // Repositories
233
232
  export * from './repository/solid-base.repository'
234
233
  export * from './repository/security-rule.repository'
package/src/interfaces.ts CHANGED
@@ -162,4 +162,8 @@ export interface QueuesModuleOptions {
162
162
  name: string;
163
163
  type: BrokerType;
164
164
  queueName: string;
165
- }
165
+ }
166
+
167
+ export type MediaWithFullUrl = Media & {
168
+ _full_url: string;
169
+ };
@@ -11,7 +11,6 @@ import { FileService } from 'src/services/file.service';
11
11
  import { CrudHelperService } from 'src/services/crud-helper.service';
12
12
 
13
13
  import { ChatterMessageDetails } from '../entities/chatter-message-details.entity';
14
- import { UserContextService } from './user-context.service';
15
14
 
16
15
  @Injectable()
17
16
  export class ChatterMessageDetailsService extends CRUDService<ChatterMessageDetails>{
@@ -27,8 +26,7 @@ export class ChatterMessageDetailsService extends CRUDService<ChatterMessageDeta
27
26
  @InjectRepository(ChatterMessageDetails, 'default')
28
27
  readonly repo: Repository<ChatterMessageDetails>,
29
28
  readonly moduleRef: ModuleRef,
30
- readonly userContextService: UserContextService
31
29
  ) {
32
- super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService, entityManager, repo, 'chatterMessageDetails', 'solid-core', moduleRef, userContextService);
30
+ super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService, entityManager, repo, 'chatterMessageDetails', 'solid-core', moduleRef);
33
31
  }
34
32
  }
@@ -16,7 +16,7 @@ import { getMediaStorageProvider } from './mediaStorageProviders';
16
16
  import { MediaStorageProviderType } from '../dtos/create-media-storage-provider-metadata.dto';
17
17
  import { ChatterMessageDetails } from '../entities/chatter-message-details.entity';
18
18
  import { ModelMetadata } from 'src/entities/model-metadata.entity';
19
- import { UserContextService } from './user-context.service';
19
+ import { RequestContextService } from './request-context.service';
20
20
  @Injectable()
21
21
  export class ChatterMessageService extends CRUDService<ChatterMessage>{
22
22
  constructor(
@@ -35,24 +35,27 @@ export class ChatterMessageService extends CRUDService<ChatterMessage>{
35
35
  readonly moduleRef: ModuleRef,
36
36
  @InjectRepository(ModelMetadata)
37
37
  private readonly modelMetadataRepo: Repository<ModelMetadata>,
38
- readonly userContextService: UserContextService
38
+ readonly requestContextService: RequestContextService
39
39
  ) {
40
- super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService,entityManager, repo, 'chatterMessage', 'solid-core', moduleRef, userContextService);
40
+ super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService,entityManager, repo, 'chatterMessage', 'solid-core', moduleRef);
41
41
  }
42
42
 
43
- async postMessage(postDto: PostChatterMessageDto, solidRequestContext: SolidRequestContextDto, files: Express.Multer.File[] = []) {
43
+ async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) {
44
44
  const chatterMessage = new ChatterMessage();
45
45
  chatterMessage.messageType = 'custom';
46
- chatterMessage.messageSubType = postDto.messageSubType || 'general';
46
+ chatterMessage.messageSubType = postDto.messageSubType || 'post_message';
47
47
  chatterMessage.messageBody = postDto.messageBody;
48
48
  chatterMessage.coModelEntityId = postDto.coModelEntityId;
49
49
  chatterMessage.coModelName = postDto.coModelName;
50
50
 
51
- const userId = typeof solidRequestContext.activeUser === 'object'
52
- ? solidRequestContext.activeUser.sub
53
- : solidRequestContext.activeUser;
54
-
55
- chatterMessage.user = { id: userId } as any;
51
+ const activeUser = this.requestContextService.getActiveUser();
52
+
53
+ if (activeUser) {
54
+ const userId = activeUser?.sub;
55
+ chatterMessage.user = { id: userId } as any;
56
+ } else {
57
+ chatterMessage.user = null;
58
+ }
56
59
 
57
60
  const savedMessage = await this.repo.save(chatterMessage);
58
61
 
@@ -81,7 +84,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage>{
81
84
  return savedMessage;
82
85
  }
83
86
 
84
- async postAuditMessageOnInsert(entity: any, metadata: EntityMetadata, activeUser: any, messageQueue: boolean = false) {
87
+ async postAuditMessageOnInsert(entity: any, metadata: EntityMetadata, messageQueue: boolean = false) {
85
88
  const model = await this.modelMetadataRepo.findOne({
86
89
  where: {
87
90
  displayName: metadata.name
@@ -98,9 +101,12 @@ export class ChatterMessageService extends CRUDService<ChatterMessage>{
98
101
 
99
102
  const auditFields = model.fields.filter(field =>
100
103
  field.enableAuditTracking &&
101
- !['oneToMany', 'mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(field.type)
104
+ !['mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(field.type) &&
105
+ !(field.type === 'relation' && field.relationType === 'one-to-many')
102
106
  );
103
107
 
108
+ const activeUser = this.requestContextService.getActiveUser();
109
+
104
110
  const chatterMessage = new ChatterMessage();
105
111
  chatterMessage.messageType = 'audit';
106
112
  chatterMessage.messageSubType = 'insert';
@@ -109,7 +115,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage>{
109
115
  chatterMessage.messageBody = `New ${model.displayName} created`;
110
116
 
111
117
  if (activeUser) {
112
- const userId = typeof activeUser === 'object' ? activeUser.sub : activeUser;
118
+ const userId = activeUser?.sub;
113
119
  chatterMessage.user = { id: userId } as any;
114
120
  } else {
115
121
  chatterMessage.user = null;
@@ -132,7 +138,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage>{
132
138
  }
133
139
  }
134
140
 
135
- async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEntity: any, activeUser: any, messageQueue: boolean = false) {
141
+ async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEntity: any, messageQueue: boolean = false) {
136
142
  const model = await this.modelMetadataRepo.findOne({
137
143
  where: {
138
144
  displayName: metadata.name
@@ -149,7 +155,8 @@ async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEn
149
155
 
150
156
  const auditFields = model.fields.filter(field =>
151
157
  field.enableAuditTracking &&
152
- !['oneToMany', 'mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(field.type)
158
+ !['mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(field.type) &&
159
+ !(field.type === 'relation' && field.relationType === 'one-to-many')
153
160
  );
154
161
 
155
162
  const relationFields = auditFields.filter(field =>
@@ -164,16 +171,16 @@ async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEn
164
171
  databaseEntity = populatedEntity;
165
172
  }
166
173
  }
167
-
168
174
  const changedFields = auditFields.filter(field => {
169
175
  const newValue = entity[field.name];
170
176
  const oldValue = databaseEntity[field.name];
171
177
  return this.hasValueChanged(newValue, oldValue);
172
178
  });
173
-
179
+
174
180
  if (changedFields.length === 0) {
175
181
  return;
176
182
  }
183
+ const activeUser = this.requestContextService.getActiveUser();
177
184
 
178
185
  const chatterMessage = new ChatterMessage();
179
186
  chatterMessage.messageType = 'audit';
@@ -183,7 +190,7 @@ async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEn
183
190
  chatterMessage.messageBody = `${model.displayName} updated`;
184
191
 
185
192
  if (activeUser) {
186
- const userId = typeof activeUser === 'object' ? activeUser.sub : activeUser;
193
+ const userId = activeUser?.sub;
187
194
  chatterMessage.user = { id: userId } as any;
188
195
  } else {
189
196
  chatterMessage.user = null;
@@ -203,7 +210,7 @@ async postAuditMessageOnUpdate(entity: any, metadata: EntityMetadata, databaseEn
203
210
  }
204
211
  }
205
212
 
206
- async postAuditMessageOnDelete(entity: any, metadata: EntityMetadata, databaseEntity: any, activeUser: any, messageQueue: boolean = false) {
213
+ async postAuditMessageOnDelete(entity: any, metadata: EntityMetadata, databaseEntity: any, messageQueue: boolean = false) {
207
214
  const model = await this.modelMetadataRepo.findOne({
208
215
  where: {
209
216
  displayName: metadata.name
@@ -225,8 +232,10 @@ async postAuditMessageOnDelete(entity: any, metadata: EntityMetadata, databaseEn
225
232
  chatterMessage.coModelName = model.singularName;
226
233
  chatterMessage.messageBody = `${model.displayName} deleted`;
227
234
 
235
+ const activeUser = this.requestContextService.getActiveUser();
236
+
228
237
  if (activeUser) {
229
- const userId = typeof activeUser === 'object' ? activeUser.sub : activeUser;
238
+ const userId = activeUser?.sub;
230
239
  chatterMessage.user = { id: userId } as any;
231
240
  } else {
232
241
  chatterMessage.user = null;
@@ -158,11 +158,18 @@ export class CrudHelperService {
158
158
 
159
159
  buildFilterQuery(qb: SelectQueryBuilder<any>, basicFilterDto: BasicFilterDto, entityAlias: string): SelectQueryBuilder<any> { //TODO : Check how to pass a type to SelectQueryBuilder instead of any
160
160
  let { limit, offset, showSoftDeleted, filters } = basicFilterDto;
161
- const { fields, sort, groupBy, populate = [] } = basicFilterDto;
161
+ const { fields, sort, groupBy, populate = [], populateMedia=[] } = basicFilterDto;
162
162
 
163
163
  // Normalize the fields, sort, groupBy and populate options i.e (since they can be either a string or an array of strings, when coming from the request)
164
164
  const normalizedFields = this.normalize(fields);
165
165
  const normalizedPopulate = this.normalize(populate);
166
+ const normalizedPopulateMedia = this.normalize(populateMedia);
167
+
168
+ // if normalizedPopulateMedia, has any nested media paths, then add then to populate excluding the last part
169
+ const additionalPopulate = this.additionalRelationsRequiredForMediaPopulation(normalizedPopulateMedia);
170
+ // Add the additional populate relations to the normalizedPopulate, if they are not already present
171
+ normalizedPopulate.push(...additionalPopulate.filter((relation) => !normalizedPopulate.includes(relation)));
172
+
166
173
  const normalizedSort = this.normalize(sort);
167
174
  const normalizedGroupBy = this.normalize(groupBy);
168
175
  if (normalizedGroupBy.length > 1) {
@@ -223,6 +230,17 @@ export class CrudHelperService {
223
230
  return qb;
224
231
  }
225
232
 
233
+ additionalRelationsRequiredForMediaPopulation(normalizedPopulateMedia: string[]) {
234
+ // Populate relations containing the media field
235
+ return normalizedPopulateMedia
236
+ .filter(pm => pm.includes("."))
237
+ .map((pm) => {
238
+ const mediaPathParts = pm.split('.');
239
+ if (mediaPathParts.length <= 1) return pm;
240
+ return mediaPathParts.slice(0, -1).join('.');
241
+ });
242
+ }
243
+
226
244
  private buildPopulateQuery(normalizedPopulate: string[], entityAlias: string, qb: SelectQueryBuilder<any>) {
227
245
  normalizedPopulate.forEach((relation) => {
228
246
  this.buildJoinQueryForRelation(qb, entityAlias, relation);
@@ -1,4 +1,4 @@
1
- import { BadRequestException } from "@nestjs/common";
1
+ import { BadRequestException, Optional } from "@nestjs/common";
2
2
  import { ConfigService } from "@nestjs/config";
3
3
  import { DiscoveryService, ModuleRef } from "@nestjs/core";
4
4
  import { EntityManager, In, IsNull, Not, QueryFailedError, SelectQueryBuilder } from "typeorm";
@@ -28,14 +28,12 @@ import { SelectionDynamicFieldCrudManager } from "../helpers/field-crud-managers
28
28
  import { SelectionStaticFieldCrudManager } from "../helpers/field-crud-managers/SelectionStaticFieldCrudManager";
29
29
  import { ShortTextFieldCrudManager } from "../helpers/field-crud-managers/ShortTextFieldCrudManager";
30
30
  import { UUIDFieldCrudManager } from "../helpers/field-crud-managers/UUIDFieldCrudManager";
31
- import { FieldCrudManager } from "../interfaces";
31
+ import { FieldCrudManager, MediaWithFullUrl } from "../interfaces";
32
32
  import { CrudHelperService } from "./crud-helper.service";
33
33
  import { FileService } from "./file.service";
34
34
  import { getMediaStorageProvider } from "./mediaStorageProviders";
35
35
  import { ModelMetadataService } from "./model-metadata.service";
36
36
  import { ModuleMetadataService } from "./module-metadata.service";
37
- import { UserContextService } from "./user-context.service";
38
- import { Optional } from "@nestjs/common";
39
37
  const DEFAULT_LIMIT = 10;
40
38
  const DEFAULT_OFFSET = 0;
41
39
  export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDto, so we get the proper types in our service
@@ -52,7 +50,6 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
52
50
  readonly modelName: string,
53
51
  readonly moduleName: string,
54
52
  readonly moduleRef: ModuleRef,
55
- @Optional() readonly userContextService?: UserContextService
56
53
  //We can just have the Model Entity here
57
54
  ) { }
58
55
 
@@ -68,9 +65,6 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
68
65
  // Check wheather user has create permission for model
69
66
  if (solidRequestContext.activeUser) {
70
67
  const hasPermission = this.crudHelperService.hasCreatePermissionOnModel(solidRequestContext.activeUser, model.singularName);
71
- if (this.userContextService) {
72
- this.userContextService.setUser(solidRequestContext.activeUser);
73
- }
74
68
  if (!hasPermission) {
75
69
  throw new BadRequestException('Forbidden');
76
70
  }
@@ -165,9 +159,6 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
165
159
  // Check wheather user has update permission for model
166
160
  if (solidRequestContext.activeUser) {
167
161
  const hasPermission = this.crudHelperService.hasUpdatePermissionOnModel(solidRequestContext.activeUser, model.singularName);
168
- if (this.userContextService) {
169
- this.userContextService.setUser(solidRequestContext.activeUser);
170
- }
171
162
  if (!hasPermission) {
172
163
  throw new BadRequestException('Forbidden');
173
164
  }
@@ -496,42 +487,85 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
496
487
  return r;
497
488
  }
498
489
 
499
- private async handlePopulateMedia(populateMedia: string[], entities: T[]) {
500
- const model = await this.modelMetadataService.findOneBySingularName(this.modelName, {
501
- fields: {
502
- model: true,
503
- mediaStorageProvider: true,
490
+ private async handlePopulateMedia(populateMedia: string[], entities: T[]) {
491
+ const model = await this.entityManager.getRepository(ModelMetadata).findOne({
492
+ where: {
493
+ singularName: this.modelName,
504
494
  },
505
- module: true,
495
+ relations: ['fields', 'fields.mediaStorageProvider', 'fields.model','module'],
506
496
  });
507
497
 
508
- const mediaFields = model.fields.filter(field => (field.type === 'mediaSingle' || field.type === 'mediaMultiple') && populateMedia.includes(field.name)
509
- );
510
-
511
- if (mediaFields.length > 0) {
512
- // Map over all entities and retrieve media in parallel for each entity
513
- await Promise.all(entities.map(async (entity) => {
514
- const mediaObj: Record<string, any> = {};
515
- // Retrieve media for each media field in parallel
516
- const media = await Promise.all(mediaFields.map(async (mediaField) => {
517
- const storageProviderMetadata = mediaField.mediaStorageProvider;
518
- const storageProviderType = storageProviderMetadata.type as MediaStorageProviderType;
519
- const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
520
- const mediaResult = await storageProvider.retrieve(entity, mediaField);
521
- mediaObj[mediaField.name] = mediaResult;
522
- }));
523
-
524
- // If media is found, assign to _media field
525
- if (media.length > 0) {
526
- entity['_media'] = mediaObj;
527
- }
528
- }));
498
+ // Will iterate through every entity & all populateMedia & call getMediaDetails for each field
499
+ for (const entity of entities) {
500
+ const mediaObj: Record<string, any> = {};
501
+ for (const mediaFieldPath of populateMedia) {
502
+ mediaObj[mediaFieldPath] = await this.getMediaObject(mediaFieldPath, model, entity);
503
+ }
504
+ entity['_media'] = mediaObj;
505
+ }
506
+ return entities;
507
+ }
508
+
509
+ private async getMediaObject(mediaFieldPath: string, model: ModelMetadata, entity: T) {
510
+ if (mediaFieldPath.includes('.')) { // mediaFieldPath is a nested field
511
+ const pathParts = mediaFieldPath.split('.');
512
+ const mediaFieldMetadata = await this.getFieldMetadataRecursively(pathParts, model.fields);
513
+ if (!mediaFieldMetadata) {
514
+ throw new BadRequestException(`Media field ${mediaFieldPath} not found in model ${this.modelName}`);
515
+ }
516
+
517
+ // We can assume that the media field entity model is already populated as part of the entity data
518
+ const mediaFieldEntity = this.getMediaFieldEntity(entity, pathParts);
519
+ if (!mediaFieldEntity) {
520
+ throw new BadRequestException(`Media field path ${mediaFieldPath} is not populated in model ${this.modelName}`);
521
+ }
522
+ const mediaWithFullUrl = await this.getMediaWithFullUrl(mediaFieldEntity, mediaFieldMetadata);
523
+ return mediaWithFullUrl;
524
+ }
525
+ else {
526
+ // mediaFieldPath is a single field
527
+ const mediaFieldMetadata = model.fields.find(field => field.name === mediaFieldPath);
528
+ if (!mediaFieldMetadata) {
529
+ throw new BadRequestException(`Media field ${mediaFieldPath} not found in model ${this.modelName}`);
530
+ }
531
+ const mediaWithFullUrl = await this.getMediaWithFullUrl(entity, mediaFieldMetadata);
532
+ return mediaWithFullUrl;
533
+ }
534
+ }
535
+
536
+ private getMediaFieldEntity(entity: T, mediaPathParts: string[]) {
537
+ let mediaFieldEntity = entity;
538
+ for (let i = 0; i < mediaPathParts.length - 1; i++) {
539
+ const pathPart = mediaPathParts[i];
540
+ if (mediaFieldEntity[pathPart]) {
541
+ mediaFieldEntity = mediaFieldEntity[pathPart];
542
+ } else {
543
+ throw new BadRequestException(`Media field ${pathPart} not found in entity ${JSON.stringify(entity)}`);
544
+ }
529
545
  }
546
+ return mediaFieldEntity;
547
+ }
548
+
549
+ async getMediaWithFullUrl(mediaEntity: any, mediaFieldMetadata: FieldMetadata): Promise<MediaWithFullUrl>{
550
+ const storageProviderMetadata = mediaFieldMetadata.mediaStorageProvider;
551
+ const storageProviderType = storageProviderMetadata.type as MediaStorageProviderType;
552
+ const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
553
+ const mediaDetails = await storageProvider.retrieve(mediaEntity, mediaFieldMetadata);
554
+ return mediaDetails as MediaWithFullUrl;
530
555
  }
531
556
 
532
557
  async findOne(id: number, query: any, solidRequestContext: any = {}) {
533
558
  const { populate = [], fields = [], populateMedia = [] } = query;
534
559
 
560
+ // const normalizedFields = this.crudHelperService.normalize(fields);
561
+ const normalizedPopulate = this.crudHelperService.normalize(populate);
562
+ const normalizedPopulateMedia = this.crudHelperService.normalize(populateMedia);
563
+
564
+ // if normalizedPopulateMedia, has any nested media paths, then add then to populate excluding the last part
565
+ const additionalPopulate = this.crudHelperService.additionalRelationsRequiredForMediaPopulation(normalizedPopulateMedia);
566
+ // Add the additional populate relations to the normalizedPopulate, if they are not already present
567
+ normalizedPopulate.push(...additionalPopulate.filter((relation) => !normalizedPopulate.includes(relation)));
568
+
535
569
  const model = await this.loadModel();
536
570
  // Check wheather user has update permission for model
537
571
  if (solidRequestContext.activeUser) {
@@ -541,42 +575,21 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
541
575
  }
542
576
  }
543
577
 
544
- const entity = await this.repo.findOne({
578
+ let entity = await this.repo.findOne({
545
579
  where: {
546
580
  //@ts-ignore
547
581
  id: id,
548
582
  },
549
- relations: populate,
583
+ relations: normalizedPopulate,
550
584
  select: fields,
551
585
  });
552
586
  if (!entity) {
553
587
  throw new Error(`Entity [${this.moduleName}.${this.modelName}] with id ${id} not found`);
554
588
  }
555
589
  // Populate the entity with the media
556
- if (populateMedia.length > 0) {
557
- const model = await this.modelMetadataService.findOneBySingularName(this.modelName, {
558
- fields: {
559
- model: true,
560
- mediaStorageProvider: true,
561
- },
562
- module: true,
563
- });
564
- const mediaObj: Record<string, any> = {};
565
- for (const mediaField of model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple')) {
566
- if (!populateMedia.includes(mediaField.name)) {
567
- continue;
568
- }
569
- const storageProviderMetadata = mediaField.mediaStorageProvider;
570
- const storageProviderType = storageProviderMetadata.type as MediaStorageProviderType;
571
- const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType);
572
- const mediaResult = await storageProvider.retrieve(entity, mediaField);
573
- let obj = { [mediaField.name]: mediaResult }
574
- mediaObj[mediaField.name] = mediaResult;
575
- // entity['media'][mediaField.name] = await storageProvider.retrieve(entity, mediaField);
576
- }
577
- if (Object.keys(mediaObj).length > 0) {
578
- entity['_media'] = mediaObj;
579
- }
590
+ if (normalizedPopulateMedia.length > 0) {
591
+ const populatedEntities = await this.handlePopulateMedia(normalizedPopulateMedia, [entity]);
592
+ entity = populatedEntities[0] as Awaited<T>;
580
593
  }
581
594
  return entity;
582
595
  }
@@ -775,4 +788,37 @@ export class CRUDService<T> { // Add two generic value i.e Person,CreatePersonDt
775
788
  }
776
789
 
777
790
 
791
+ async getFieldMetadataRecursively(pathParts: string[], fields: FieldMetadata[]) {
792
+ if (!pathParts || pathParts.length === 0) {
793
+ throw new BadRequestException('Path parts cannot be empty');
794
+ }
795
+
796
+ const [currentPart, ...remainingParts] = pathParts;
797
+ const field = fields.find(field => field.name === currentPart);
798
+
799
+ if (!field) {
800
+ throw new BadRequestException(`Field ${currentPart} not found in model ${this.modelName}`);
801
+ }
802
+
803
+ // Base case: last part, return the field
804
+ if (remainingParts.length === 0) {
805
+ return field;
806
+ }
807
+
808
+ if (!field.relationCoModelSingularName) {
809
+ throw new BadRequestException(`Field ${field.name} does not define a relationCoModelSingularName`);
810
+ }
811
+
812
+ const relationCoModel = await this.entityManager.getRepository(ModelMetadata).findOne({
813
+ where: { singularName: field.relationCoModelSingularName },
814
+ relations: ['fields', 'fields.mediaStorageProvider', 'fields.model'],
815
+ });
816
+
817
+ if (!relationCoModel) {
818
+ throw new BadRequestException(`Model ${field.relationCoModelSingularName} not found`);
819
+ }
820
+
821
+ return this.getFieldMetadataRecursively(remainingParts, relationCoModel.fields);
822
+ }
778
823
  }
824
+
@@ -503,6 +503,7 @@ export class FieldMetadataService {
503
503
  "encryptionType",
504
504
  "decryptWhen",
505
505
  "columnName",
506
+ "enableAuditTracking"
506
507
  ];
507
508
 
508
509
  case SolidFieldType.bigint:
@@ -521,7 +522,8 @@ export class FieldMetadataService {
521
522
  "encrypt",
522
523
  "encryptionType",
523
524
  "decryptWhen",
524
- "columnName"
525
+ "columnName",
526
+ "enableAuditTracking"
525
527
  ];
526
528
 
527
529
  // case SolidFieldType.float:
@@ -560,7 +562,8 @@ export class FieldMetadataService {
560
562
  "encrypt",
561
563
  "encryptionType",
562
564
  "decryptWhen",
563
- "columnName"
565
+ "columnName",
566
+ "enableAuditTracking"
564
567
  ];
565
568
 
566
569
  case SolidFieldType.shortText:
@@ -582,7 +585,8 @@ export class FieldMetadataService {
582
585
  "encryptionType",
583
586
  "decryptWhen",
584
587
  "columnName",
585
- "isUserKey"
588
+ "isUserKey",
589
+ "enableAuditTracking"
586
590
  ];
587
591
 
588
592
  case SolidFieldType.longtext:
@@ -665,7 +669,8 @@ export class FieldMetadataService {
665
669
  "encrypt",
666
670
  "encryptionType",
667
671
  "decryptWhen",
668
- "columnName"
672
+ "columnName",
673
+ "enableAuditTracking"
669
674
  ];
670
675
 
671
676
  case SolidFieldType.date:
@@ -684,7 +689,8 @@ export class FieldMetadataService {
684
689
  "encrypt",
685
690
  "encryptionType",
686
691
  "decryptWhen",
687
- "columnName"
692
+ "columnName",
693
+ "enableAuditTracking"
688
694
  ];
689
695
 
690
696
  case SolidFieldType.datetime:
@@ -703,7 +709,8 @@ export class FieldMetadataService {
703
709
  "encrypt",
704
710
  "encryptionType",
705
711
  "decryptWhen",
706
- "columnName"
712
+ "columnName",
713
+ "enableAuditTracking"
707
714
  ];
708
715
 
709
716
  case SolidFieldType.time:
@@ -722,7 +729,8 @@ export class FieldMetadataService {
722
729
  "encrypt",
723
730
  "encryptionType",
724
731
  "decryptWhen",
725
- "columnName"
732
+ "columnName",
733
+ "enableAuditTracking"
726
734
  ];
727
735
 
728
736
  case SolidFieldType.relation:
@@ -750,7 +758,8 @@ export class FieldMetadataService {
750
758
  "columnName",
751
759
  "relationJoinTableName",
752
760
  "isRelationManyToManyOwner",
753
- "relationFieldFixedFilter"
761
+ "relationFieldFixedFilter",
762
+ "enableAuditTracking"
754
763
  ];
755
764
 
756
765
  case SolidFieldType.mediaSingle:
@@ -812,7 +821,8 @@ export class FieldMetadataService {
812
821
  "encrypt",
813
822
  "encryptionType",
814
823
  "decryptWhen",
815
- "columnName"
824
+ "columnName",
825
+ "enableAuditTracking"
816
826
  ];
817
827
 
818
828
  case SolidFieldType.password:
@@ -855,7 +865,8 @@ export class FieldMetadataService {
855
865
  "encrypt",
856
866
  "encryptionType",
857
867
  "decryptWhen",
858
- "columnName"
868
+ "columnName",
869
+ "enableAuditTracking"
859
870
  ];
860
871
 
861
872
  case SolidFieldType.selectionDynamic:
@@ -877,8 +888,8 @@ export class FieldMetadataService {
877
888
  "encryptionType",
878
889
  "decryptWhen",
879
890
  "columnName",
880
- "isUserKey"
881
-
891
+ "isUserKey",
892
+ "enableAuditTracking"
882
893
  ];
883
894
  case SolidFieldType.computed:
884
895
  return [