@solidstarters/solid-core 1.2.87 → 1.2.89

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 (37) hide show
  1. package/dist/controllers/export-template.controller.d.ts +3 -2
  2. package/dist/controllers/export-template.controller.d.ts.map +1 -1
  3. package/dist/controllers/export-template.controller.js +23 -10
  4. package/dist/controllers/export-template.controller.js.map +1 -1
  5. package/dist/dtos/export.dto.d.ts +5 -0
  6. package/dist/dtos/export.dto.d.ts.map +1 -0
  7. package/dist/dtos/export.dto.js +12 -0
  8. package/dist/dtos/export.dto.js.map +1 -0
  9. package/dist/entities/export-transaction.entity.js +1 -1
  10. package/dist/entities/export-transaction.entity.js.map +1 -1
  11. package/dist/seeders/seed-data/solid-core-metadata.json +1 -1
  12. package/dist/services/authentication.service.d.ts +2 -0
  13. package/dist/services/authentication.service.d.ts.map +1 -1
  14. package/dist/services/authentication.service.js +20 -15
  15. package/dist/services/authentication.service.js.map +1 -1
  16. package/dist/services/export-template.service.d.ts +3 -2
  17. package/dist/services/export-template.service.d.ts.map +1 -1
  18. package/dist/services/export-template.service.js +6 -6
  19. package/dist/services/export-template.service.js.map +1 -1
  20. package/dist/services/export-transaction.service.d.ts +6 -3
  21. package/dist/services/export-transaction.service.d.ts.map +1 -1
  22. package/dist/services/export-transaction.service.js +101 -22
  23. package/dist/services/export-transaction.service.js.map +1 -1
  24. package/dist/services/refresh-token-ids-storage.service.d.ts +6 -3
  25. package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
  26. package/dist/services/refresh-token-ids-storage.service.js +52 -7
  27. package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/controllers/export-template.controller.ts +20 -7
  31. package/src/dtos/export.dto.ts +5 -0
  32. package/src/entities/export-transaction.entity.ts +1 -1
  33. package/src/seeders/seed-data/solid-core-metadata.json +1 -1
  34. package/src/services/authentication.service.ts +44 -23
  35. package/src/services/export-template.service.ts +7 -6
  36. package/src/services/export-transaction.service.ts +136 -25
  37. package/src/services/refresh-token-ids-storage.service.ts +90 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solidstarters/solid-core",
3
- "version": "1.2.87",
3
+ "version": "1.2.89",
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",
@@ -5,6 +5,7 @@ import { ExportTemplateService } from '../services/export-template.service';
5
5
  import { CreateExportTemplateDto } from '../dtos/create-export-template.dto';
6
6
  import { UpdateExportTemplateDto } from '../dtos/update-export-template.dto';
7
7
  import { Response } from 'express';
8
+ import { StartExportSyncDto } from 'src/dtos/export.dto';
8
9
 
9
10
  @ApiTags('Solid')
10
11
  @Controller('export-template') //FIXME: Change this to the model plural name
@@ -74,13 +75,19 @@ export class ExportTemplateController {
74
75
  }
75
76
 
76
77
  @ApiBearerAuth("jwt")
77
- @Post(':id/startExport/sync')
78
- async startExportSync(@Param('id') id: number, @Res() res: Response) {
79
- const exportFileInfo = await this.service.startExportSync(+id);
78
+ @Post('/startExport/sync')
79
+ async startExportSync(@Body() dto: StartExportSyncDto, @Res() res: Response) {
80
+ const { filters, ...rest } = dto;
81
+ let updateDto = { ...rest };
82
+ // Check if templateName is present → create template
83
+ if (updateDto?.templateName) {
84
+ const newTemplate = await this.service.create(updateDto, []);
85
+ updateDto = { ...updateDto, id: newTemplate.id };
86
+ }
87
+ const exportFileInfo = await this.service.startExportSync(updateDto, filters);
80
88
  if (exportFileInfo.exportStream === null) {
81
89
  throw new InternalServerErrorException("Export stream is null");
82
90
  }
83
-
84
91
  // ✅ Set response headers for streaming
85
92
  res.setHeader('Content-Disposition', `attachment; filename="${exportFileInfo.fileName}"`);
86
93
  res.setHeader('Content-Type', exportFileInfo.mimeType);
@@ -90,9 +97,15 @@ export class ExportTemplateController {
90
97
  }
91
98
 
92
99
  @ApiBearerAuth("jwt")
93
- @Post(':id/startExport/async')
94
- async startExportAsync(@Param('id') id: number) {
95
- return this.service.startExportAsync(+id);
100
+ @Post('/startExport/async')
101
+ async startExportAsync(@Body() dto: StartExportSyncDto) {
102
+ const { filters, ...rest } = dto;
103
+ let updateDto = { ...rest };
104
+ if (updateDto.templateName) {
105
+ const newTemplate = await this.service.create(updateDto, []);
106
+ updateDto = { ...updateDto, id: newTemplate.id };
107
+ }
108
+ return this.service.startExportAsync(updateDto, filters);
96
109
  }
97
110
 
98
111
  }
@@ -0,0 +1,5 @@
1
+ import { UpdateExportTemplateDto } from "./update-export-template.dto";
2
+
3
+ export class StartExportSyncDto extends UpdateExportTemplateDto {
4
+ filters?: any;
5
+ }
@@ -17,6 +17,6 @@ export class ExportTransaction extends CommonEntity {
17
17
  @Column({ type: "text", nullable: true })
18
18
  error: string;
19
19
  @Index()
20
- @ManyToOne(() => ExportTemplate, { onDelete: "CASCADE", nullable: false })
20
+ @ManyToOne(() => ExportTemplate, { onDelete: "CASCADE", nullable: true })
21
21
  exportTemplate: ExportTemplate;
22
22
  }
@@ -3564,7 +3564,7 @@
3564
3564
  "displayName": "Related Export Template",
3565
3565
  "type": "relation",
3566
3566
  "ormType": "int",
3567
- "required": true,
3567
+ "required": false,
3568
3568
  "unique": false,
3569
3569
  "index": true,
3570
3570
  "private": false,
@@ -859,31 +859,45 @@ export class AuthenticationService {
859
859
  }
860
860
 
861
861
  async generateTokens(user: User) {
862
- const refreshTokenId = randomUUID();
863
-
864
- // const userRoleNames = user.roles.map((role) => role.name).join(';')
865
- const userRoleNames = user.roles.map((role) => role.name);
866
862
 
867
863
  const [accessToken, refreshToken] = await Promise.all([
868
- this.signToken<Partial<ActiveUserData>>(
869
- user.id,
870
- this.jwtConfiguration.accessTokenTtl,
871
- { username: user.username, email: user.email, roles: userRoleNames },
872
- ),
873
- this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
874
- refreshTokenId,
875
- }),
864
+ this.generateAccessToken(user),
865
+ this.generateRefreshToken(user),
876
866
  ]);
877
867
 
878
- // store the refresh token id in the redis storage.
879
- await this.refreshTokenIdsStorage.insert(user.id, refreshTokenId);
880
-
881
868
  return {
882
869
  accessToken,
883
870
  refreshToken,
884
871
  };
885
872
  }
886
873
 
874
+ async generateAccessToken(user: User) {
875
+
876
+ // const userRoleNames = user.roles.map((role) => role.name).join(';')
877
+ const userRoleNames = user.roles.map((role) => role.name);
878
+
879
+ const accessToken = await this.signToken<Partial<ActiveUserData>>(
880
+ user.id,
881
+ this.jwtConfiguration.accessTokenTtl,
882
+ { username: user.username, email: user.email, roles: userRoleNames },
883
+ );
884
+
885
+ return accessToken;
886
+ }
887
+
888
+ async generateRefreshToken(user: User) {
889
+ const refreshTokenId = randomUUID();
890
+
891
+ const refreshToken = await this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
892
+ refreshTokenId,
893
+ })
894
+
895
+ // store the refresh token id in the redis storage.
896
+ await this.refreshTokenIdsStorage.insert(user.id, refreshToken);
897
+
898
+ return refreshToken;
899
+ }
900
+
887
901
  async refreshTokens(refreshTokenDto: RefreshTokenDto) {
888
902
  try {
889
903
  const { sub, refreshTokenId } = await this.jwtService.verifyAsync<Pick<ActiveUserData, 'sub'> & { refreshTokenId: string }>(refreshTokenDto.refreshToken, {
@@ -904,14 +918,21 @@ export class AuthenticationService {
904
918
  throw new UnauthorizedException();
905
919
  }
906
920
 
907
- const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
908
- if (isValid) {
909
- // Refresh token rotation.
910
- await this.refreshTokenIdsStorage.invalidate(user.id);
911
- } else {
912
- throw new Error('Refresh token is invalid');
913
- }
914
- return this.generateTokens(user);
921
+ // TODO: Replace the if else condition below with a call to validateAndRotate - Done
922
+ // const isValid = await this.refreshTokenIdsStorage.validate(user.id, refreshTokenId);
923
+ // if (isValid) {
924
+ // // Refresh token rotation.
925
+ // await this.refreshTokenIdsStorage.invalidate(user.id);
926
+ // } else {
927
+ // throw new Error('Refresh token is invalid');
928
+ // }
929
+
930
+ const currentRefreshToken = await this.refreshTokenIdsStorage.validateAndRotate(user, refreshTokenDto.refreshToken);
931
+
932
+ return {
933
+ accessToken: await this.generateAccessToken(user),
934
+ refreshToken: currentRefreshToken,
935
+ };
915
936
  } catch (err) {
916
937
  if (err instanceof InvalidatedRefreshTokenError) {
917
938
  // Take action: notify user that his refresh token might have been stolen?
@@ -18,35 +18,36 @@ import { ExportTransaction } from 'src/entities/export-transaction.entity';
18
18
  import { Readable } from 'stream';
19
19
  import { ExportTemplate } from '../entities/export-template.entity';
20
20
  import { ExportTransactionFileInfo, ExportTransactionService } from './export-transaction.service';
21
+ import { UpdateExportTemplateDto } from 'src/dtos/update-export-template.dto';
21
22
 
22
23
  @Injectable()
23
24
  export class ExportTemplateService extends CRUDService<ExportTemplate>{
24
- async startExportSync(id: number): Promise<ExportTransactionFileInfo> {
25
+ async startExportSync(updateDto: UpdateExportTemplateDto, filters:any): Promise<ExportTransactionFileInfo> {
25
26
  // Create the export transaction entry, with status 'started'
26
27
  const exportTransaction: CreateExportTransactionDto = await this.exportTransactionService.toDto({
27
28
  datetime: new Date(),
28
29
  status: 'started',
29
- exportTemplateId: id,
30
+ exportTemplateId: updateDto?.id ? updateDto?.id : null,
30
31
  });
31
32
  const exportTransactionEntity = await this.exportTransactionService.create(exportTransaction);
32
33
 
33
34
  // Trigger the export process
34
- const exportFileInfo = await this.exportTransactionService.triggerExportSync(exportTransactionEntity.id);
35
+ const exportFileInfo = await this.exportTransactionService.triggerExportSync(exportTransactionEntity.id, exportTransactionEntity, updateDto, filters);
35
36
  // It should return the export transaction id
36
37
  return exportFileInfo;
37
38
  }
38
39
 
39
- async startExportAsync(id: number): Promise<ExportTransaction>{
40
+ async startExportAsync(updateDto: UpdateExportTemplateDto, filters:any): Promise<ExportTransaction>{
40
41
  // Create the export transaction entry, with status 'started'
41
42
  const exportTransaction: CreateExportTransactionDto = await this.exportTransactionService.toDto({
42
43
  datetime: new Date(),
43
44
  status: 'started',
44
- exportTemplateId: id,
45
+ exportTemplateId: updateDto?.id ? updateDto?.id : null,
45
46
  });
46
47
  const exportTransactionEntity = await this.exportTransactionService.create(exportTransaction);
47
48
 
48
49
  // Trigger the export process
49
- this.exportTransactionService.triggerExportAsync(exportTransactionEntity.id);
50
+ this.exportTransactionService.triggerExportAsync(exportTransactionEntity.id, exportTransactionEntity, updateDto, filters);
50
51
 
51
52
  // It should return the export transaction id, so client can use this to check the status
52
53
  return exportTransactionEntity;
@@ -26,6 +26,8 @@ import { CsvService } from './csv.service';
26
26
  import { ExcelService } from './excel.service';
27
27
  import { getMediaStorageProvider } from './mediaStorageProviders';
28
28
  import { SolidIntrospectService } from './solid-introspect.service';
29
+ import { ModelMetadata } from 'src/entities/model-metadata.entity';
30
+ import { UpdateExportTemplateDto } from 'src/dtos/update-export-template.dto';
29
31
 
30
32
  const EXPORT_CHUNK_SIZE = 100;
31
33
  enum ExportStatus {
@@ -67,19 +69,30 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
67
69
  // readonly fieldMetadataService: FieldMetadataService,
68
70
  @InjectRepository(FieldMetadata, 'default')
69
71
  readonly fieldRepo: Repository<FieldMetadata>,
72
+ @InjectRepository(ModelMetadata, 'default')
73
+ readonly ModelMetadataRepo : Repository<ModelMetadata>,
70
74
  readonly moduleRef: ModuleRef
71
75
  ) {
72
76
  super(modelMetadataService, moduleMetadataService, configService, fileService, discoveryService, crudHelperService, entityManager, repo, 'exportTransaction', 'solid-core',moduleRef);
73
77
  }
74
78
 
75
79
  // Return the export stream
76
- async triggerExportSync(id: number): Promise<ExportTransactionFileInfo> {
80
+ async triggerExportSync(id: number, exportTransactionEntity: any, updateDto: UpdateExportTemplateDto , filters: any): Promise<ExportTransactionFileInfo> {
77
81
  try {
78
- const loadedExportTransaction = await this.loadExportTransaction(id);
79
- const { exportStream, templateName, uuid, exportTransaction } = await this.getExportStreamDetails(loadedExportTransaction);
82
+ // const loadedExportTransaction = await this.loadExportTransaction(id);
83
+ // from updateDto, get modelId and get modelMetadata
84
+ const modeldata = await this.ModelMetadataRepo.findOne({
85
+ where: { id: updateDto?.modelMetadataId},
86
+ relations: { fields: true},
87
+ })
88
+ const modelName = modeldata?.singularName;
89
+ const modelTemplateName = modelName;
90
+ const fields = JSON.parse(updateDto?.fields);
91
+ const templateFormat = updateDto?.templateFormat;
92
+ const { exportStream, templateName, uuid, exportTransaction } = await this.getExportStreamDetails(modelName, modelTemplateName, fields, modeldata, templateFormat, id, exportTransactionEntity, filters);
80
93
  this.updateExportTransaction(id, ExportStatus.COMPLETED);
81
- const fileName = this.getFileName(templateName, uuid, loadedExportTransaction.exportTemplate.templateFormat);
82
- const mimeType = this.getMimeType(loadedExportTransaction.exportTemplate.templateFormat);
94
+ const fileName = this.getFileName(templateName, uuid, templateFormat);
95
+ const mimeType = this.getMimeType(templateFormat);
83
96
  return { exportStream, fileName, mimeType, exportTransaction };
84
97
  } catch (error) {
85
98
  this.updateExportTransaction(id, ExportStatus.FAILED, error.message);
@@ -88,13 +101,22 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
88
101
  }
89
102
 
90
103
  // Store the export stream using the appropriate storage provider
91
- async triggerExportAsync(id: number): Promise<void> {
104
+ async triggerExportAsync(id: number, exportTransactionEntity: any, updateDto: UpdateExportTemplateDto, filters:any): Promise<void> {
92
105
  try {
93
- const loadedExportTransaction = await this.loadExportTransaction(id)
94
- const { exportStream, templateName, uuid, exportTransaction } = await this.getExportStreamDetails(loadedExportTransaction);
95
- const fileFormat = loadedExportTransaction.exportTemplate.templateFormat;
106
+ // const loadedExportTransaction = await this.loadExportTransaction(id)
107
+ // from updateDto, get modelId and get modelMetadata
108
+ const modeldata = await this.ModelMetadataRepo.findOne({
109
+ where: { id: updateDto?.modelMetadataId},
110
+ relations: { fields: true},
111
+ })
112
+ const modelName = modeldata?.singularName;
113
+ const modelTemplateName = modelName;
114
+ const fields = JSON.parse(updateDto?.fields);
115
+ const templateFormat = updateDto?.templateFormat;
116
+ const { exportStream, templateName, uuid, exportTransaction } = await this.getExportStreamDetails(modelName, modelTemplateName, fields, modeldata, templateFormat, id, exportTransactionEntity, filters);
117
+ // const fileFormat = loadedExportTransaction.exportTemplate.templateFormat;
96
118
  // Store the file using the appropriate storage provider
97
- await this.storeExportStream(exportStream, exportTransaction, this.getFileName(templateName, uuid, fileFormat));
119
+ await this.storeExportStream(exportStream, exportTransaction, this.getFileName(templateName, uuid, templateFormat));
98
120
  this.updateExportTransaction(id, ExportStatus.COMPLETED);
99
121
  } catch (error) {
100
122
  this.updateExportTransaction(id, ExportStatus.FAILED, error.message);
@@ -106,7 +128,7 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
106
128
  private async loadExportTransaction(id: number) {
107
129
  return await this.repo.findOne({
108
130
  where: { id: id },
109
- relations: { exportTemplate: { modelMetadata: true } },
131
+ relations: { exportTemplate: { modelMetadata: {fields: true} }},
110
132
  }
111
133
  );
112
134
  }
@@ -115,22 +137,26 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
115
137
  await this.repo.update(id, { status, error });
116
138
  }
117
139
 
118
- private async getExportStreamDetails(exportTransaction: ExportTransaction) {
140
+ private async getExportStreamDetails(modelName: string, templateName: string, fields:any, modelData:any, templateFormat:string, id:number, exportTransaction: any, filters: any) {
119
141
  // Get the columns which need to be exported & the model id
120
- const fields = JSON.parse(exportTransaction.exportTemplate.fields);
121
-
122
- // Get the appropriate service for the model by trying to fetch a model service matching a particular name
123
- const modelName = exportTransaction.exportTemplate.modelMetadata.singularName;
124
- const modelService = this.introspectService.getProvider(`${classify(modelName)}Service`);
125
- const templateName = exportTransaction.exportTemplate.templateName;
126
- const uuid = exportTransaction.exportTransactionId; //TODO can be renamed to exportTransactionUUID
142
+ // const fields = JSON.parse(exportTransaction.exportTemplate.fields);
127
143
 
144
+ // // Get the appropriate service for the model by trying to fetch a model service matching a particular name
145
+ // const modelName = exportTransaction.exportTemplate.modelMetadata.singularName;
146
+ const modelService = this.introspectService.getProvider(`${classify(modelName)}Service`);
147
+ // const templateName = exportTransaction.exportTemplate.templateName;
148
+ const uuid = String(id); //TODO can be renamed to exportTransactionUUID
149
+ // const modelData = exportTransaction.exportTemplate.modelMetadata;
128
150
 
129
151
  // Get the data records function
130
- const dataRecordsFunc = await this.getDataRecordsFunc(fields, modelService);
152
+ //const dataRecordsFunc = await this.getDataRecordsFunc(fields, modelService,modelData, filters);
153
+ const dataRecordsFunc = await this.getDataRecordsFunc(fields, modelService, modelData, filters);
131
154
 
132
155
  // Get the export passthru stream (since it is a passthru stream, nothing is stored in memory & it is streamed directly when the stream is read)
133
- let exportStream = await this.getExportStream(exportTransaction.exportTemplate.templateFormat, dataRecordsFunc);
156
+ // let exportStream = await this.getExportStream(exportTransaction.exportTemplate.templateFormat, dataRecordsFunc);
157
+ // return { exportStream, templateName, uuid, exportTransaction };
158
+
159
+ let exportStream = await this.getExportStream(templateFormat, dataRecordsFunc);
134
160
  return { exportStream, templateName, uuid, exportTransaction };
135
161
  }
136
162
 
@@ -181,20 +207,78 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
181
207
  return (fileFormat === ExportFormat.EXCEL) ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'text/csv';
182
208
  }
183
209
 
184
- private async getDataRecordsFunc(fields: any, modelService: InstanceWrapper<any>): Promise<(chunkIndex: number, chunkSize: number) => Promise<any[]>> {
210
+ private async getDataRecordsFunc(fields: any, modelService: InstanceWrapper<any>, modelMetadata: any, filters:any): Promise<(chunkIndex: number, chunkSize: number) => Promise<any[]>> {
185
211
  // Return a function which will take the chunkIndex & chunkSize and return the data
212
+ // Get the relation fields to populate
213
+ const relatedFieldNames = modelMetadata?.fields
214
+ .filter((field: { relationType: any; }) => field.relationType !== null)
215
+ .map((field: { name: any; }) => field.name);
216
+
217
+ //Get the model metadata of relation field with userKey details
218
+ const relatedModelsUserKeyMap = new Map<string, string>();
219
+ for (const field of modelMetadata?.fields || []) {
220
+ if (field.relationType && field.relationCoModelSingularName && fields.includes(field.name)) {
221
+ const relatedModelMetadata = await this.ModelMetadataRepo.findOne({
222
+ where: { singularName: field.relationCoModelSingularName },
223
+ relations: ['userKeyField'],
224
+ });
225
+
226
+ if (relatedModelMetadata?.userKeyField?.name) {
227
+ relatedModelsUserKeyMap.set(field.name, relatedModelMetadata.userKeyField.name);
228
+ }
229
+ }
230
+ }
231
+
186
232
  return async (chunkIndex: number, chunkSize: number) => {
187
233
  const offset = chunkIndex * chunkSize;
188
234
  const recordFilterDto: BasicFilterDto = {
189
- fields,
190
235
  limit: chunkSize,
191
236
  offset,
237
+ populate: relatedFieldNames
192
238
  };
239
+ const cleanedFilters = cleanNullsFromObject(filters);
240
+
241
+ if (cleanedFilters && Object.keys(cleanedFilters).length > 0) {
242
+ recordFilterDto.filters = cleanedFilters;
243
+ }
244
+
245
+ //Get the non relation fields which are in fields array passed to this function
246
+ const nonRelationalFieldSet = new Set(
247
+ modelMetadata?.fields
248
+ .filter((field: { name: any; relationType: any; }) => fields.includes(field.name) && field.relationType === null)
249
+ .map((field: { name: any; }) => field.name)
250
+ );
193
251
  const data = await modelService.instance.find(recordFilterDto);
194
252
  const records = data.records ?? [];
195
- return records;
196
- }
253
+ const cleanedRecords = records.map((record: Record<string, any>) => {
254
+ const newRecord: Record<string, any> = {};
255
+
256
+ // Include non-relational fields
257
+ for (const key of nonRelationalFieldSet as Set<string>) {
258
+ newRecord[key] = record[key];
259
+ }
260
+
261
+ // Include userKey from each related field
262
+ for (const [relatedFieldName, userKeyFieldName] of relatedModelsUserKeyMap.entries()) {
263
+ const relatedData = record[relatedFieldName];
264
+
265
+ if (Array.isArray(relatedData)) {
266
+ // For many-to-many or one-to-many
267
+ const values = relatedData.map(item => item?.[userKeyFieldName]).filter(Boolean);
268
+ newRecord[relatedFieldName] = values.join(', ');
269
+ } else if (relatedData && typeof relatedData === 'object') {
270
+ // For many-to-one or one-to-one
271
+ newRecord[relatedFieldName] = relatedData?.[userKeyFieldName] ?? null;
272
+ } else {
273
+ newRecord[relatedFieldName] = null;
274
+ }
275
+ }
276
+
277
+ return newRecord;
278
+ });
279
+ return cleanedRecords
197
280
  }
281
+ }
198
282
 
199
283
  async toDto(data: Partial<CreateExportTransactionDto>): Promise<CreateExportTransactionDto> {
200
284
  const dto = new CreateExportTransactionDto(data);
@@ -206,3 +290,30 @@ export class ExportTransactionService extends CRUDService<ExportTransaction> {
206
290
  return dto;
207
291
  }
208
292
  }
293
+
294
+ function cleanNullsFromObject(obj: any): any {
295
+ if (Array.isArray(obj)) {
296
+ return obj
297
+ .filter(item => item !== null && item !== undefined)
298
+ .map(cleanNullsFromObject);
299
+ } else if (typeof obj === 'object' && obj !== null) {
300
+ const newObj: any = {};
301
+ for (const key in obj) {
302
+ const value = obj[key];
303
+ if (value !== null && value !== undefined) {
304
+ const cleanedValue = cleanNullsFromObject(value);
305
+ // Only assign non-empty objects/arrays or non-null primitives
306
+ if (
307
+ (typeof cleanedValue === 'object' && Object.keys(cleanedValue).length > 0) ||
308
+ (Array.isArray(cleanedValue) && cleanedValue.length > 0) ||
309
+ typeof cleanedValue !== 'object'
310
+ ) {
311
+ newObj[key] = cleanedValue;
312
+ }
313
+ }
314
+ }
315
+ return newObj;
316
+ }
317
+ return obj;
318
+ }
319
+
@@ -1,6 +1,7 @@
1
1
  import { CACHE_MANAGER } from '@nestjs/cache-manager';
2
- import { Inject, Injectable } from '@nestjs/common';
2
+ import { Inject, Injectable, forwardRef } from '@nestjs/common';
3
3
  import { Cache } from 'cache-manager';
4
+ import { AuthenticationService } from './authentication.service';
4
5
 
5
6
  // TODO: Ideally this should be in a separate file - putting this here for brevity
6
7
  export class InvalidatedRefreshTokenError extends Error { }
@@ -21,24 +22,106 @@ export class RefreshTokenIdsStorageService {
21
22
  // return this.redisClient.quit();
22
23
  // }
23
24
 
24
- constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
25
+ constructor(
26
+ @Inject(CACHE_MANAGER) private cacheManager: Cache,
27
+ @Inject(forwardRef(() => AuthenticationService))
28
+ private readonly authenticationService: AuthenticationService
29
+ ) { }
25
30
 
26
- async insert(userId: number, tokenId: string): Promise<void> {
27
- await this.cacheManager.set(this.getKey(userId), tokenId);
31
+ async insert(userId: number, refreshToken: string): Promise<void> {
32
+ // TODO: save a refresh token object with this shape {"currentRefreshToken": "", "previousRefreshToken": ""}
33
+ // Save a refresh token object with the shape: { currentRefreshToken: string, previousRefreshToken: string }
34
+ const existing = (await this.cacheManager.get(this.getKey(userId))) as { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
35
+ const refreshTokenState = {
36
+ currentRefreshToken: refreshToken,
37
+ previousRefreshToken: "",
38
+ };
39
+ await this.cacheManager.set(this.getKey(userId), refreshTokenState);
28
40
  }
29
41
 
30
- async validate(userId: number, tokenId: string): Promise<boolean> {
42
+ async validate(userId: number, refreshToken: string): Promise<boolean> {
43
+ // TODO: Assume you get this shape out of the cache {"currentRefreshToken": "", "previousRefreshToken": ""}
44
+ // Then you will compare against the currentRefreshToken.
31
45
  const storedId = await this.cacheManager.get(this.getKey(userId));
32
- if (storedId !== tokenId) {
46
+ if (storedId !== refreshToken) {
33
47
  throw new InvalidatedRefreshTokenError();
34
48
  }
35
- return storedId === tokenId;
49
+ return storedId === refreshToken;
36
50
  }
37
51
 
38
52
  async invalidate(userId: number): Promise<void> {
39
53
  await this.cacheManager.del(this.getKey(userId));
40
54
  }
41
55
 
56
+ async validateAndRotate(user: any, refreshToken: string): Promise<string> {
57
+ let valid = false;
58
+
59
+ // TODO: Assume you get this shape out of the cache {"currentRefreshToken": "", "previousRefreshToken": ""}
60
+ // Then you will compare against the currentRefreshToken.
61
+ const refreshTokenState = await this.cacheManager.get(this.getKey(user.id));
62
+ console.log("refreshTokenState", refreshTokenState);
63
+
64
+ // Use the authentication service to generate a new refresh token, set it in the currentRefreshToken in scenario 1 and return.
65
+
66
+ // if UI.refresh_token is matching with Cache.currentRefreshToken
67
+ // then invalidate (updated cache state, no need to delete anything), then generate new token and return.
68
+ // also set a setTimeout to run after X minutes, this will simply update the RefreshTokenCacheState to this object {"currentRefreshToken": "R2","justInvalidatedRefreshToken": ""}
69
+ // valid=true
70
+
71
+ // - if UI.refresh_token is matching Cache.justInvalidatedRefreshToken
72
+ // then use the Cache.currentRefreshToken, generate new access token and return.
73
+ // We do not modify the cache state at all.
74
+ // valid=true
75
+
76
+ let newRefreshToken: string | undefined;
77
+ if (
78
+ refreshTokenState &&
79
+ typeof refreshTokenState === 'object' &&
80
+ 'currentRefreshToken' in refreshTokenState &&
81
+ 'previousRefreshToken' in refreshTokenState
82
+ ) {
83
+ if (refreshTokenState.currentRefreshToken === refreshToken) {
84
+ // Scenario 1: Token matches currentRefreshToken
85
+ valid = true;
86
+ // Rotate tokens: move current to previous, set new current (simulate generation)
87
+ newRefreshToken = await this.authenticationService.generateRefreshToken(user); // Replace with real token generation logic
88
+
89
+
90
+ // updated cache state
91
+ await this.cacheManager.set(this.getKey(user.id), {
92
+ currentRefreshToken: newRefreshToken,
93
+ previousRefreshToken: refreshTokenState.currentRefreshToken,
94
+ });
95
+
96
+ // Optionally, set a timeout to clear previousRefreshToken after X minutes
97
+ setTimeout(async () => {
98
+ const state = (await this.cacheManager.get(this.getKey(user.id))) as any;
99
+ if (state && state.currentRefreshToken === newRefreshToken) {
100
+ await this.cacheManager.set(this.getKey(user.id), {
101
+ currentRefreshToken: newRefreshToken,
102
+ previousRefreshToken: "",
103
+ });
104
+ }
105
+ }, 1 * 60 * 1000); // 5 minutes
106
+ } else if (refreshTokenState.previousRefreshToken === refreshToken) {
107
+ // Scenario 2: Token matches previousRefreshToken
108
+ valid = true;
109
+ // Do not modify cache
110
+ // Generate new refresh token based on currentRefreshToken
111
+ const existingRefreshTokenState = (await this.cacheManager.get(this.getKey(user.id))) as { currentRefreshToken?: string, previousRefreshToken?: string } | undefined;
112
+ newRefreshToken = existingRefreshTokenState?.currentRefreshToken;
113
+ }
114
+ }
115
+
116
+
117
+ if (!valid) {
118
+ throw new InvalidatedRefreshTokenError();
119
+ }
120
+
121
+ // TODO: return the refresh token either currentRefreshToken
122
+ return newRefreshToken; // Fallback to the provided tokenId if no new token was generated
123
+ }
124
+
42
125
  private getKey(userId: number): string {
43
126
  return `user-${userId}`;
44
127
  }