@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.
- package/dist/controllers/export-template.controller.d.ts +3 -2
- package/dist/controllers/export-template.controller.d.ts.map +1 -1
- package/dist/controllers/export-template.controller.js +23 -10
- package/dist/controllers/export-template.controller.js.map +1 -1
- package/dist/dtos/export.dto.d.ts +5 -0
- package/dist/dtos/export.dto.d.ts.map +1 -0
- package/dist/dtos/export.dto.js +12 -0
- package/dist/dtos/export.dto.js.map +1 -0
- package/dist/entities/export-transaction.entity.js +1 -1
- package/dist/entities/export-transaction.entity.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +1 -1
- package/dist/services/authentication.service.d.ts +2 -0
- package/dist/services/authentication.service.d.ts.map +1 -1
- package/dist/services/authentication.service.js +20 -15
- package/dist/services/authentication.service.js.map +1 -1
- package/dist/services/export-template.service.d.ts +3 -2
- package/dist/services/export-template.service.d.ts.map +1 -1
- package/dist/services/export-template.service.js +6 -6
- package/dist/services/export-template.service.js.map +1 -1
- package/dist/services/export-transaction.service.d.ts +6 -3
- package/dist/services/export-transaction.service.d.ts.map +1 -1
- package/dist/services/export-transaction.service.js +101 -22
- package/dist/services/export-transaction.service.js.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.d.ts +6 -3
- package/dist/services/refresh-token-ids-storage.service.d.ts.map +1 -1
- package/dist/services/refresh-token-ids-storage.service.js +52 -7
- package/dist/services/refresh-token-ids-storage.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/controllers/export-template.controller.ts +20 -7
- package/src/dtos/export.dto.ts +5 -0
- package/src/entities/export-transaction.entity.ts +1 -1
- package/src/seeders/seed-data/solid-core-metadata.json +1 -1
- package/src/services/authentication.service.ts +44 -23
- package/src/services/export-template.service.ts +7 -6
- package/src/services/export-transaction.service.ts +136 -25
- 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.
|
|
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('
|
|
78
|
-
async startExportSync(@
|
|
79
|
-
const
|
|
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('
|
|
94
|
-
async startExportAsync(@
|
|
95
|
-
|
|
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
|
}
|
|
@@ -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:
|
|
20
|
+
@ManyToOne(() => ExportTemplate, { onDelete: "CASCADE", nullable: true })
|
|
21
21
|
exportTemplate: ExportTemplate;
|
|
22
22
|
}
|
|
@@ -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.
|
|
869
|
-
|
|
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
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
82
|
-
const mimeType = this.getMimeType(
|
|
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
|
-
|
|
95
|
-
const
|
|
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,
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
27
|
-
|
|
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,
|
|
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 !==
|
|
46
|
+
if (storedId !== refreshToken) {
|
|
33
47
|
throw new InvalidatedRefreshTokenError();
|
|
34
48
|
}
|
|
35
|
-
return storedId ===
|
|
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
|
}
|