@solidxai/core 0.1.6-beta.24 → 0.1.6-beta.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/dist/entities/mq-message-queue.entity.d.ts.map +1 -1
- package/dist/entities/mq-message-queue.entity.js.map +1 -1
- package/dist/helpers/cors.helper.d.ts.map +1 -1
- package/dist/helpers/cors.helper.js +8 -1
- package/dist/helpers/cors.helper.js.map +1 -1
- package/dist/jobs/chatter-queue-options.js +1 -1
- package/dist/jobs/chatter-queue-options.js.map +1 -1
- package/dist/jobs/chatter-queue-publisher.service.d.ts +9 -9
- package/dist/jobs/chatter-queue-publisher.service.d.ts.map +1 -1
- package/dist/jobs/chatter-queue-publisher.service.js +5 -5
- package/dist/jobs/chatter-queue-publisher.service.js.map +1 -1
- package/dist/jobs/chatter-queue-subscriber.service.d.ts +4 -4
- package/dist/jobs/chatter-queue-subscriber.service.d.ts.map +1 -1
- package/dist/jobs/chatter-queue-subscriber.service.js +11 -11
- package/dist/jobs/chatter-queue-subscriber.service.js.map +1 -1
- package/dist/jobs/database/chatter-queue-options-database.d.ts +8 -0
- package/dist/jobs/database/chatter-queue-options-database.d.ts.map +1 -0
- package/dist/jobs/database/chatter-queue-options-database.js +10 -0
- package/dist/jobs/database/chatter-queue-options-database.js.map +1 -0
- package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts +12 -0
- package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts.map +1 -0
- package/dist/jobs/database/chatter-queue-publisher-database.service.js +39 -0
- package/dist/jobs/database/chatter-queue-publisher-database.service.js.map +1 -0
- package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts +19 -0
- package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -0
- package/dist/jobs/database/chatter-queue-subscriber-database.service.js +62 -0
- package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -0
- package/dist/services/chatter-message.service.d.ts +4 -4
- package/dist/services/chatter-message.service.d.ts.map +1 -1
- package/dist/services/chatter-message.service.js +33 -9
- package/dist/services/chatter-message.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +8 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/subscribers/audit.subscriber.d.ts +8 -3
- package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
- package/dist/subscribers/audit.subscriber.js +54 -52
- package/dist/subscribers/audit.subscriber.js.map +1 -1
- package/package.json +1 -1
- package/src/entities/mq-message-queue.entity.ts +8 -8
- package/src/helpers/cors.helper.ts +14 -2
- package/src/jobs/chatter-queue-options.ts +1 -1
- package/src/jobs/chatter-queue-publisher.service.ts +11 -11
- package/src/jobs/chatter-queue-subscriber.service.ts +13 -8
- package/src/jobs/database/chatter-queue-options-database.ts +9 -0
- package/src/jobs/database/chatter-queue-publisher-database.service.ts +24 -0
- package/src/jobs/database/chatter-queue-subscriber-database.service.ts +53 -0
- package/src/services/1.js +6 -0
- package/src/services/chatter-message.service.ts +41 -9
- package/src/solid-core.module.ts +8 -2
- package/src/subscribers/audit.subscriber.ts +59 -224
- package/dist-tests/api/authenticate.spec.js +0 -119
- package/dist-tests/api/authenticate.spec.js.map +0 -1
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
- package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
- package/dist-tests/api/ping.spec.js +0 -21
- package/dist-tests/api/ping.spec.js.map +0 -1
- package/dist-tests/helpers/auth.js +0 -41
- package/dist-tests/helpers/auth.js.map +0 -1
- package/dist-tests/helpers/env.js +0 -11
- package/dist-tests/helpers/env.js.map +0 -1
|
@@ -7,21 +7,21 @@ import { MqMessageService } from '../services/mq-message.service';
|
|
|
7
7
|
import { QueuesModuleOptions } from "../interfaces";
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
export type
|
|
10
|
+
export type AuditEventType = 'insert' | 'update' | 'delete';
|
|
11
11
|
|
|
12
|
-
export interface
|
|
13
|
-
eventType:
|
|
14
|
-
|
|
15
|
-
entityId: string
|
|
16
|
-
occurredAt: string;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
userId?:
|
|
12
|
+
export interface AuditQueuePayload {
|
|
13
|
+
eventType: AuditEventType;
|
|
14
|
+
modelName: string; // TypeORM entity class name (e.g. 'Order')
|
|
15
|
+
entityId: string | number | null;
|
|
16
|
+
occurredAt: string; // ISO timestamp, captured at event time
|
|
17
|
+
after?: any; // entity state after operation (insert/update)
|
|
18
|
+
before?: any; // entity state before operation (update/delete)
|
|
19
|
+
updatedColumnNames?: string[]; // propertyNames of changed columns (update only)
|
|
20
|
+
userId?: number | null; // active user captured at event time
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
@Injectable()
|
|
24
|
-
export class
|
|
24
|
+
export class ChatterQueuePublisherRabbitmq extends RabbitMqPublisher<AuditQueuePayload> {
|
|
25
25
|
constructor(
|
|
26
26
|
protected readonly mqMessageService: MqMessageService,
|
|
27
27
|
protected readonly mqMessageQueueService: MqMessageQueueService,
|
|
@@ -6,12 +6,12 @@ import { MqMessageService } from '../services/mq-message.service';
|
|
|
6
6
|
import { MqMessageQueueService } from '../services/mq-message-queue.service';
|
|
7
7
|
import { QueuesModuleOptions } from "../interfaces";
|
|
8
8
|
import chatterQueueOptions from './chatter-queue-options';
|
|
9
|
-
import {
|
|
9
|
+
import { AuditQueuePayload } from './chatter-queue-publisher.service';
|
|
10
10
|
import { ChatterMessageService } from 'src/services/chatter-message.service';
|
|
11
11
|
|
|
12
12
|
@Injectable()
|
|
13
|
-
export class
|
|
14
|
-
private readonly
|
|
13
|
+
export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber<AuditQueuePayload> {
|
|
14
|
+
private readonly chatterLogger = new Logger(ChatterQueueSubscriberRabbitmq.name);
|
|
15
15
|
|
|
16
16
|
constructor(
|
|
17
17
|
readonly mqMessageService: MqMessageService,
|
|
@@ -27,19 +27,24 @@ export class ChatterQueueSubscriber extends RabbitMqSubscriber<any> {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
async subscribe(message: QueueMessage<
|
|
30
|
+
async subscribe(message: QueueMessage<AuditQueuePayload>) {
|
|
31
31
|
const p = message.payload;
|
|
32
|
-
this.
|
|
32
|
+
this.chatterLogger.debug(`Audit event ${p.eventType} ${p.modelName}#${p.entityId}`);
|
|
33
33
|
|
|
34
34
|
switch (p.eventType) {
|
|
35
35
|
case 'insert':
|
|
36
|
-
await this.chatterMessageService.postAuditMessageOnInsert(p.after,
|
|
36
|
+
await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName);
|
|
37
37
|
break;
|
|
38
38
|
case 'update':
|
|
39
|
-
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
39
|
+
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
40
|
+
p.after,
|
|
41
|
+
p.modelName,
|
|
42
|
+
p.before,
|
|
43
|
+
(p.updatedColumnNames ?? []).map(n => ({ propertyName: n })),
|
|
44
|
+
);
|
|
40
45
|
break;
|
|
41
46
|
case 'delete':
|
|
42
|
-
await this.chatterMessageService.postAuditMessageOnDelete(p.
|
|
47
|
+
await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before);
|
|
43
48
|
break;
|
|
44
49
|
}
|
|
45
50
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { DatabasePublisher } from 'src/services/queues/database-publisher.service';
|
|
4
|
+
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
5
|
+
import { MqMessageService } from '../../services/mq-message.service';
|
|
6
|
+
import { QueuesModuleOptions } from "../../interfaces";
|
|
7
|
+
import { AuditQueuePayload } from '../chatter-queue-publisher.service';
|
|
8
|
+
import chatterQueueOptionsDatabase from './chatter-queue-options-database';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class ChatterQueuePublisherDatabase extends DatabasePublisher<AuditQueuePayload> {
|
|
12
|
+
constructor(
|
|
13
|
+
protected readonly mqMessageService: MqMessageService,
|
|
14
|
+
protected readonly mqMessageQueueService: MqMessageQueueService,
|
|
15
|
+
) {
|
|
16
|
+
super(mqMessageService, mqMessageQueueService);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
options(): QueuesModuleOptions {
|
|
20
|
+
return {
|
|
21
|
+
...chatterQueueOptionsDatabase
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { DatabaseSubscriber } from 'src/services/queues/database-subscriber.service';
|
|
4
|
+
import { QueueMessage } from 'src/interfaces/mq';
|
|
5
|
+
import { MqMessageService } from '../../services/mq-message.service';
|
|
6
|
+
import { MqMessageQueueService } from '../../services/mq-message-queue.service';
|
|
7
|
+
import { QueuesModuleOptions } from "../../interfaces";
|
|
8
|
+
import { PollerService } from 'src/services/poller.service';
|
|
9
|
+
import { AuditQueuePayload } from '../chatter-queue-publisher.service';
|
|
10
|
+
import { ChatterMessageService } from 'src/services/chatter-message.service';
|
|
11
|
+
import chatterQueueOptionsDatabase from './chatter-queue-options-database';
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class ChatterQueueSubscriberDatabase extends DatabaseSubscriber<AuditQueuePayload> {
|
|
15
|
+
private readonly chatterLogger = new Logger(ChatterQueueSubscriberDatabase.name);
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
readonly mqMessageService: MqMessageService,
|
|
19
|
+
readonly mqMessageQueueService: MqMessageQueueService,
|
|
20
|
+
readonly poller: PollerService,
|
|
21
|
+
private readonly chatterMessageService: ChatterMessageService,
|
|
22
|
+
) {
|
|
23
|
+
super(mqMessageService, mqMessageQueueService, poller);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
options(): QueuesModuleOptions {
|
|
27
|
+
return {
|
|
28
|
+
...chatterQueueOptionsDatabase
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async subscribe(message: QueueMessage<AuditQueuePayload>) {
|
|
33
|
+
const p = message.payload;
|
|
34
|
+
this.chatterLogger.debug(`Audit event ${p.eventType} ${p.modelName}#${p.entityId}`);
|
|
35
|
+
|
|
36
|
+
switch (p.eventType) {
|
|
37
|
+
case 'insert':
|
|
38
|
+
await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName);
|
|
39
|
+
break;
|
|
40
|
+
case 'update':
|
|
41
|
+
await this.chatterMessageService.postAuditMessageOnUpdate(
|
|
42
|
+
p.after,
|
|
43
|
+
p.modelName,
|
|
44
|
+
p.before,
|
|
45
|
+
(p.updatedColumnNames ?? []).map(n => ({ propertyName: n })),
|
|
46
|
+
);
|
|
47
|
+
break;
|
|
48
|
+
case 'delete':
|
|
49
|
+
await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
1. Do i need to create a storeStreams method for aws service too?
|
|
2
|
+
- Handle later
|
|
3
|
+
2. queues handling -> if queues is enabled by default, i.e triggerExport(exportTransactionEntity.id).
|
|
4
|
+
- startExport should either return the data or return the transaction id
|
|
5
|
+
3. How to handle scenarios wherein, nested related exist.(do i need to only get the userkey)
|
|
6
|
+
- show the userKey
|
|
@@ -90,13 +90,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
90
90
|
return savedMessage;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
async postAuditMessageOnInsert(entity: any,
|
|
93
|
+
async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false) {
|
|
94
94
|
if (!entity) {
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
97
|
const model = await this.modelMetadataRepo.findOne({
|
|
98
98
|
where: {
|
|
99
|
-
singularName: lowerFirst(
|
|
99
|
+
singularName: lowerFirst(modelName)
|
|
100
100
|
},
|
|
101
101
|
relations: {
|
|
102
102
|
fields: true,
|
|
@@ -152,13 +152,13 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
async postAuditMessageOnUpdate(entity: any,
|
|
155
|
+
async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false) {
|
|
156
156
|
if (!databaseEntity || !entity) {
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
const model = await this.modelMetadataRepo.findOne({
|
|
160
160
|
where: {
|
|
161
|
-
singularName: lowerFirst(
|
|
161
|
+
singularName: lowerFirst(modelName)
|
|
162
162
|
},
|
|
163
163
|
relations: {
|
|
164
164
|
fields: true,
|
|
@@ -204,6 +204,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
204
204
|
|
|
205
205
|
const changedRelationFields = [];
|
|
206
206
|
if (potentialRelationFields.length > 0) {
|
|
207
|
+
const metadata = this.entityManager.connection.entityMetadatas.find(m => m.name === modelName);
|
|
207
208
|
const populatedOldEntity = await this.populateRelationFields(databaseEntity, potentialRelationFields, metadata);
|
|
208
209
|
|
|
209
210
|
for (const field of potentialRelationFields) {
|
|
@@ -268,13 +269,12 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
|
|
271
|
-
async postAuditMessageOnDelete(
|
|
272
|
+
async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false) {
|
|
272
273
|
const model = await this.modelMetadataRepo.findOne({
|
|
273
274
|
where: {
|
|
274
|
-
singularName: lowerFirst(
|
|
275
|
+
singularName: lowerFirst(modelName)
|
|
275
276
|
},
|
|
276
277
|
relations: {
|
|
277
|
-
fields: true,
|
|
278
278
|
module: true,
|
|
279
279
|
userKeyField: true
|
|
280
280
|
}
|
|
@@ -284,13 +284,29 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
const modelFields = await this.modelMetadataHelperService.loadFieldHierarchy(model.singularName);
|
|
288
|
+
|
|
289
|
+
const auditFields = modelFields.filter(field =>
|
|
290
|
+
field.enableAuditTracking &&
|
|
291
|
+
!['mediaSingle', 'mediaMultiple', 'richText', 'json'].includes(field.type) &&
|
|
292
|
+
!(field.type === 'relation' && field.relationType === 'one-to-many')
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Populate relation fields so display values (e.g. names) are resolvable.
|
|
296
|
+
// The related entities themselves still exist in the DB after a delete.
|
|
297
|
+
const relationFields = auditFields.filter(field => field.type === 'relation');
|
|
298
|
+
const entityMetadata = this.entityManager.connection.entityMetadatas.find(m => m.name === modelName);
|
|
299
|
+
const populatedEntity = relationFields.length > 0 && entityMetadata
|
|
300
|
+
? await this.populateRelationFields(databaseEntity, relationFields, entityMetadata)
|
|
301
|
+
: { ...databaseEntity };
|
|
302
|
+
|
|
287
303
|
const chatterMessage = new ChatterMessage();
|
|
288
304
|
chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT;
|
|
289
305
|
chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_DELETE;
|
|
290
306
|
chatterMessage.coModelEntityId = databaseEntity?.id;
|
|
291
307
|
chatterMessage.coModelName = model?.singularName;
|
|
292
308
|
chatterMessage.modelDisplayName = model?.displayName;
|
|
293
|
-
chatterMessage.modelUserKey =
|
|
309
|
+
chatterMessage.modelUserKey = databaseEntity[model?.userKeyField?.name];
|
|
294
310
|
chatterMessage.messageBody = `${model?.displayName} deleted`;
|
|
295
311
|
|
|
296
312
|
const activeUser = this.requestContextService.getActiveUser();
|
|
@@ -302,7 +318,23 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
|
|
|
302
318
|
chatterMessage.user = null;
|
|
303
319
|
}
|
|
304
320
|
|
|
305
|
-
await this.repo.save(chatterMessage);
|
|
321
|
+
const savedMessage = await this.repo.save(chatterMessage);
|
|
322
|
+
|
|
323
|
+
for (const field of auditFields) {
|
|
324
|
+
const fieldValue = populatedEntity[field.name];
|
|
325
|
+
if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
|
|
326
|
+
const messageDetail = new ChatterMessageDetails();
|
|
327
|
+
messageDetail.chatterMessage = savedMessage;
|
|
328
|
+
messageDetail.fieldName = field.name;
|
|
329
|
+
messageDetail.fieldDisplayName = field.displayName;
|
|
330
|
+
messageDetail.fieldType = field.type;
|
|
331
|
+
messageDetail.oldValue = this.formatFieldValue(field, fieldValue);
|
|
332
|
+
messageDetail.oldValueDisplay = await this.formatFieldValueDisplay(field, fieldValue);
|
|
333
|
+
messageDetail.newValue = null;
|
|
334
|
+
messageDetail.newValueDisplay = null;
|
|
335
|
+
await this.chatterMessageDetailsRepo.save(messageDetail);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
306
338
|
}
|
|
307
339
|
|
|
308
340
|
private formatFieldValue(field: any, value: any): string {
|
package/src/solid-core.module.ts
CHANGED
|
@@ -94,6 +94,10 @@ import { Msg91SmsQueuePublisher } from './jobs/msg91-sms-publisher.service';
|
|
|
94
94
|
import { Msg91SmsQueueSubscriber } from './jobs/msg91-sms-subscriber.service';
|
|
95
95
|
import { SmtpEmailQueuePublisherRabbitmq } from './jobs/smtp-email-publisher.service';
|
|
96
96
|
import { SmtpEmailQueueSubscriberRabbitmq } from './jobs/smtp-email-subscriber.service';
|
|
97
|
+
import { ChatterQueuePublisherRabbitmq } from './jobs/chatter-queue-publisher.service';
|
|
98
|
+
import { ChatterQueueSubscriberRabbitmq } from './jobs/chatter-queue-subscriber.service';
|
|
99
|
+
import { ChatterQueuePublisherDatabase } from './jobs/database/chatter-queue-publisher-database.service';
|
|
100
|
+
import { ChatterQueueSubscriberDatabase } from './jobs/database/chatter-queue-subscriber-database.service';
|
|
97
101
|
import { TestQueuePublisher } from './jobs/test-queue-publisher.service';
|
|
98
102
|
import { TestQueueSubscriber } from './jobs/test-queue-subscriber.service';
|
|
99
103
|
import { UserRegistrationListener } from './listeners/user-registration.listener';
|
|
@@ -595,8 +599,10 @@ import { Entity } from 'typeorm';
|
|
|
595
599
|
TestQueuePublisher,
|
|
596
600
|
TestQueueSubscriber,
|
|
597
601
|
|
|
598
|
-
|
|
599
|
-
|
|
602
|
+
ChatterQueuePublisherRabbitmq,
|
|
603
|
+
ChatterQueueSubscriberRabbitmq,
|
|
604
|
+
ChatterQueuePublisherDatabase,
|
|
605
|
+
ChatterQueueSubscriberDatabase,
|
|
600
606
|
|
|
601
607
|
TestQueuePublisherDatabase,
|
|
602
608
|
TestQueueSubscriberDatabase,
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import { Injectable, Scope } from '@nestjs/common';
|
|
1
|
+
import { Injectable, Logger, Scope } from '@nestjs/common';
|
|
2
2
|
import { lowerFirst } from 'src/helpers/string.helper';
|
|
3
3
|
import { SolidRegistry } from 'src/helpers/solid-registry';
|
|
4
4
|
import { DataSource, EntityMetadata, EntitySubscriberInterface, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type DeferredCall =
|
|
9
|
-
| { kind: 'insert'; args: Parameters<ChatterMessageService['postAuditMessageOnInsert']> }
|
|
10
|
-
| { kind: 'update'; args: Parameters<ChatterMessageService['postAuditMessageOnUpdate']> }
|
|
11
|
-
| { kind: 'delete'; args: Parameters<ChatterMessageService['postAuditMessageOnDelete']> };
|
|
5
|
+
import { AuditQueuePayload } from 'src/jobs/chatter-queue-publisher.service';
|
|
6
|
+
import { RequestContextService } from 'src/services/request-context.service';
|
|
7
|
+
import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
|
|
12
8
|
|
|
13
9
|
@Injectable({scope: Scope.TRANSIENT})
|
|
14
|
-
// @EventSubscriber()
|
|
15
10
|
export class AuditSubscriber implements EntitySubscriberInterface {
|
|
11
|
+
private readonly logger = new Logger(AuditSubscriber.name);
|
|
16
12
|
private dataSource: DataSource;
|
|
17
13
|
constructor(
|
|
18
|
-
private readonly
|
|
14
|
+
private readonly publisherFactory: PublisherFactory<AuditQueuePayload>,
|
|
19
15
|
private readonly solidRegistry: SolidRegistry,
|
|
16
|
+
private readonly requestContextService: RequestContextService,
|
|
20
17
|
) { }
|
|
21
18
|
|
|
22
19
|
bindToDataSource(dataSource: DataSource) {
|
|
@@ -25,12 +22,12 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
// Per-transaction buffer (auto-GC when queryRunner is gone)
|
|
28
|
-
private perTxn = new WeakMap<any,
|
|
25
|
+
private perTxn = new WeakMap<any, AuditQueuePayload[]>();
|
|
29
26
|
|
|
30
|
-
private enqueue(event: { queryRunner: any },
|
|
27
|
+
private enqueue(event: { queryRunner: any }, payload: AuditQueuePayload) {
|
|
31
28
|
const qr = event.queryRunner;
|
|
32
29
|
const arr = this.perTxn.get(qr) ?? [];
|
|
33
|
-
arr.push(
|
|
30
|
+
arr.push(payload);
|
|
34
31
|
this.perTxn.set(qr, arr);
|
|
35
32
|
}
|
|
36
33
|
|
|
@@ -38,43 +35,48 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
38
35
|
return this.solidRegistry.isAuditableModel(lowerFirst(metadata.name));
|
|
39
36
|
}
|
|
40
37
|
|
|
38
|
+
private activeUserId(): number | null {
|
|
39
|
+
return this.requestContextService.getActiveUser()?.sub ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
41
42
|
async afterInsert(event: InsertEvent<any>) {
|
|
42
|
-
if (this.shouldTrackAudit(event.metadata))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
if (!this.shouldTrackAudit(event.metadata)) return;
|
|
44
|
+
this.enqueue(event, {
|
|
45
|
+
eventType: 'insert',
|
|
46
|
+
modelName: event.metadata.name,
|
|
47
|
+
entityId: event.entity?.id ?? null,
|
|
48
|
+
occurredAt: new Date().toISOString(),
|
|
49
|
+
after: event.entity ?? null,
|
|
50
|
+
userId: this.activeUserId(),
|
|
51
|
+
});
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
async afterUpdate(event: UpdateEvent<any>) {
|
|
52
|
-
if (this.shouldTrackAudit(event.metadata))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
if (!this.shouldTrackAudit(event.metadata)) return;
|
|
56
|
+
this.enqueue(event, {
|
|
57
|
+
eventType: 'update',
|
|
58
|
+
modelName: event.metadata.name,
|
|
59
|
+
entityId: event.entity?.id ?? null,
|
|
60
|
+
occurredAt: new Date().toISOString(),
|
|
61
|
+
after: event.entity ?? null,
|
|
62
|
+
// databaseEntity is only populated when the entity was fetched first (save() path).
|
|
63
|
+
// QueryBuilder update() leaves this undefined; postAuditMessageOnUpdate guards for it.
|
|
64
|
+
before: event.databaseEntity ?? null,
|
|
65
|
+
updatedColumnNames: (event.updatedColumns ?? []).map(c => c.propertyName),
|
|
66
|
+
userId: this.activeUserId(),
|
|
67
|
+
});
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
async afterRemove(event: RemoveEvent<any>) {
|
|
67
|
-
if (this.shouldTrackAudit(event.metadata))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
}
|
|
71
|
+
if (!this.shouldTrackAudit(event.metadata)) return;
|
|
72
|
+
this.enqueue(event, {
|
|
73
|
+
eventType: 'delete',
|
|
74
|
+
modelName: event.metadata.name,
|
|
75
|
+
entityId: event.databaseEntity?.id ?? null,
|
|
76
|
+
occurredAt: new Date().toISOString(),
|
|
77
|
+
before: event.databaseEntity,
|
|
78
|
+
userId: this.activeUserId(),
|
|
79
|
+
});
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// --------- transaction lifecycle ----------
|
|
@@ -82,191 +84,24 @@ export class AuditSubscriber implements EntitySubscriberInterface {
|
|
|
82
84
|
const batch = this.perTxn.get(event.queryRunner) ?? [];
|
|
83
85
|
this.perTxn.delete(event.queryRunner);
|
|
84
86
|
|
|
85
|
-
// Now
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
// Now outside the DB transaction — safe to publish to the queue.
|
|
88
|
+
// allSettled: publish in parallel; a single failure does not block the rest.
|
|
89
|
+
const results = await Promise.allSettled(
|
|
90
|
+
batch.map(payload => this.publisherFactory.publish({ payload }, 'ChatterQueuePublisher'))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
results.forEach((result, i) => {
|
|
94
|
+
if (result.status === 'rejected') {
|
|
95
|
+
this.logger.error(
|
|
96
|
+
`Failed to publish audit event for ${batch[i].modelName}#${batch[i].entityId}`,
|
|
97
|
+
result.reason,
|
|
98
|
+
);
|
|
96
99
|
}
|
|
97
|
-
}
|
|
100
|
+
});
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
afterTransactionRollback(event: { queryRunner: any }) {
|
|
101
|
-
// Drop buffered
|
|
104
|
+
// Drop buffered payloads; the write never happened.
|
|
102
105
|
this.perTxn.delete(event.queryRunner);
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
|
-
|
|
106
|
-
// import { DataSource, EntityMetadata, EntitySubscriberInterface, EventSubscriber, InsertEvent, RemoveEvent, UpdateEvent } from 'typeorm';
|
|
107
|
-
// import { Injectable } from '@nestjs/common';
|
|
108
|
-
// import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
109
|
-
// import { Repository } from 'typeorm';
|
|
110
|
-
// import { ModelMetadata } from '../entities/model-metadata.entity';
|
|
111
|
-
// import { lowerFirst } from 'src/helpers/string.helper';
|
|
112
|
-
// import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service';
|
|
113
|
-
// import { ChatterMessagePayload } from 'src/jobs/chatter-queue-publisher.service';
|
|
114
|
-
// import { RequestContextService } from 'src/services/request-context.service';
|
|
115
|
-
// import { PublisherFactory } from 'src/services/queues/publisher-factory.service';
|
|
116
|
-
|
|
117
|
-
// @EventSubscriber()
|
|
118
|
-
// @Injectable()
|
|
119
|
-
// export class AuditSubscriber implements EntitySubscriberInterface {
|
|
120
|
-
// private perTxn = new WeakMap<any, ChatterMessagePayload[]>();
|
|
121
|
-
|
|
122
|
-
// constructor(
|
|
123
|
-
// @InjectDataSource() private readonly dataSource: DataSource,
|
|
124
|
-
// @InjectRepository(ModelMetadata) private readonly modelMetadataRepo: Repository<ModelMetadata>,
|
|
125
|
-
// private readonly modelMetadataHelperService: ModelMetadataHelperService,
|
|
126
|
-
// private readonly requestContext: RequestContextService,
|
|
127
|
-
// private readonly publisherFactory: PublisherFactory<any>
|
|
128
|
-
// ) {
|
|
129
|
-
// this.dataSource.subscribers.push(this);
|
|
130
|
-
// }
|
|
131
|
-
|
|
132
|
-
// // --- small cache to avoid metadata queries on every row ---
|
|
133
|
-
// private modelCache = new Map<string, { enable: boolean; fields: Array<{ name: string; enableAuditTracking: boolean; type: string; relationType?: string }>; ts: number }>();
|
|
134
|
-
// private cacheTTLms = 60_000;
|
|
135
|
-
|
|
136
|
-
// private async shouldTrackAudit(entity: any, metadata: EntityMetadata): Promise<{ enable: boolean; auditFields?: string[] }> {
|
|
137
|
-
// const key = metadata.name;
|
|
138
|
-
// const now = Date.now();
|
|
139
|
-
// const cached = this.modelCache.get(key);
|
|
140
|
-
// if (cached && (now - cached.ts) < this.cacheTTLms) {
|
|
141
|
-
// if (!cached.enable) return { enable: false };
|
|
142
|
-
// const fields = cached.fields.filter(f =>
|
|
143
|
-
// f.enableAuditTracking &&
|
|
144
|
-
// !['mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(f.type) &&
|
|
145
|
-
// !(f.type === 'relation' && f.relationType === 'one-to-many')
|
|
146
|
-
// );
|
|
147
|
-
// const present = fields.map(f => f.name).filter(n => entity?.[n] !== undefined);
|
|
148
|
-
// return { enable: present.length > 0, auditFields: present };
|
|
149
|
-
// }
|
|
150
|
-
|
|
151
|
-
// const model = await this.modelMetadataRepo.findOne({
|
|
152
|
-
// where: { singularName: lowerFirst(metadata.name) },
|
|
153
|
-
// relations: { fields: true, module: true },
|
|
154
|
-
// });
|
|
155
|
-
// const enable = !!model?.enableAuditTracking;
|
|
156
|
-
// const fields = model?.fields ?? [];
|
|
157
|
-
// this.modelCache.set(key, { enable, fields, ts: now });
|
|
158
|
-
|
|
159
|
-
// if (!enable) return { enable: false };
|
|
160
|
-
// const filtered = fields.filter(f =>
|
|
161
|
-
// f.enableAuditTracking &&
|
|
162
|
-
// !['mediaSingle', 'mediaMultiple', 'computed', 'richText', 'json'].includes(f.type) &&
|
|
163
|
-
// !(f.type === 'relation' && f.relationType === 'one-to-many')
|
|
164
|
-
// );
|
|
165
|
-
// const present = filtered.map(f => f.name).filter(n => entity?.[n] !== undefined);
|
|
166
|
-
// return { enable: present.length > 0, auditFields: present };
|
|
167
|
-
// }
|
|
168
|
-
|
|
169
|
-
// private push(event: { queryRunner: any }, msg: ChatterMessagePayload) {
|
|
170
|
-
// const arr = this.perTxn.get(event.queryRunner) ?? [];
|
|
171
|
-
// arr.push(msg);
|
|
172
|
-
// this.perTxn.set(event.queryRunner, arr);
|
|
173
|
-
// }
|
|
174
|
-
|
|
175
|
-
// async afterInsert(event: InsertEvent<any>) {
|
|
176
|
-
// if (!event.entity) return;
|
|
177
|
-
// const enable = await this.shouldTrackAudit(event.entity, event.metadata);
|
|
178
|
-
// if (!enable) return;
|
|
179
|
-
|
|
180
|
-
// const payload: ChatterMessagePayload = {
|
|
181
|
-
// eventType: 'insert',
|
|
182
|
-
// model: event.metadata.name,
|
|
183
|
-
// entityId: String(event.entity.id ?? event.entity.uuid ?? ''),
|
|
184
|
-
// occurredAt: new Date().toISOString(),
|
|
185
|
-
// after: this.safeCopy(event.entity),
|
|
186
|
-
// userId: this.getUserId(),
|
|
187
|
-
// };
|
|
188
|
-
// this.push(event, payload);
|
|
189
|
-
// }
|
|
190
|
-
|
|
191
|
-
// async afterUpdate(event: UpdateEvent<any>) {
|
|
192
|
-
// // Updated entity may be null if you used raw query; fall back to databaseEntity
|
|
193
|
-
// const current = event.entity ?? {};
|
|
194
|
-
// const before = event.databaseEntity ?? {};
|
|
195
|
-
// const { enable, auditFields } = await this.shouldTrackAudit(current, event.metadata);
|
|
196
|
-
// if (!enable) return;
|
|
197
|
-
|
|
198
|
-
// const changedCols = (event.updatedColumns || []).map(c => c.propertyName);
|
|
199
|
-
// const payload: ChatterMessagePayload = {
|
|
200
|
-
// eventType: 'update',
|
|
201
|
-
// model: event.metadata.name,
|
|
202
|
-
// entityId: String((current as any).id ?? (before as any).id ?? ''),
|
|
203
|
-
// occurredAt: new Date().toISOString(),
|
|
204
|
-
// before: this.pick(before, auditFields || changedCols),
|
|
205
|
-
// after: this.pick(current, auditFields || changedCols),
|
|
206
|
-
// diff: changedCols,
|
|
207
|
-
// userId: this.getUserId(),
|
|
208
|
-
// };
|
|
209
|
-
// this.push(event, payload);
|
|
210
|
-
// }
|
|
211
|
-
|
|
212
|
-
// async afterRemove(event: RemoveEvent<any>) {
|
|
213
|
-
// const base = event.entity ?? event.databaseEntity;
|
|
214
|
-
// if (!base) return;
|
|
215
|
-
|
|
216
|
-
// const { enable } = await this.shouldTrackAudit(base, event.metadata);
|
|
217
|
-
// if (!enable) return;
|
|
218
|
-
|
|
219
|
-
// const payload: ChatterMessagePayload = {
|
|
220
|
-
// eventType: 'delete',
|
|
221
|
-
// model: event.metadata.name,
|
|
222
|
-
// entityId: String((base as any).id ?? ''),
|
|
223
|
-
// occurredAt: new Date().toISOString(),
|
|
224
|
-
// before: this.safeCopy(base),
|
|
225
|
-
// userId: this.getUserId(),
|
|
226
|
-
// };
|
|
227
|
-
// this.push(event, payload);
|
|
228
|
-
// }
|
|
229
|
-
|
|
230
|
-
// // Publish AFTER the transaction commits -> no idle-in-transaction
|
|
231
|
-
// async afterTransactionCommit(event: { queryRunner: any }) {
|
|
232
|
-
// const batch = this.perTxn.get(event.queryRunner) ?? [];
|
|
233
|
-
// this.perTxn.delete(event.queryRunner);
|
|
234
|
-
// for (const msg of batch) {
|
|
235
|
-
// try {
|
|
236
|
-
// await this.publisherFactory.publish({ payload: msg, parentEntity: msg.model, parentEntityId: msg.entityId }, 'ChatterQueuePublisher');
|
|
237
|
-
// } catch (err) {
|
|
238
|
-
// // log + optionally send to a DLQ or retry queue
|
|
239
|
-
// // do NOT throw; commit already happened
|
|
240
|
-
// // your RabbitMqPublisher likely tracks failures in MqMessage tables anyway
|
|
241
|
-
// }
|
|
242
|
-
// }
|
|
243
|
-
// }
|
|
244
|
-
|
|
245
|
-
// afterTransactionRollback(event: { queryRunner: any }) {
|
|
246
|
-
// this.perTxn.delete(event.queryRunner);
|
|
247
|
-
// }
|
|
248
|
-
|
|
249
|
-
// // --- small helpers to keep payloads JSON-safe and small ---
|
|
250
|
-
// private safeCopy(obj: any) {
|
|
251
|
-
// try {
|
|
252
|
-
// return JSON.parse(JSON.stringify(obj));
|
|
253
|
-
// } catch {
|
|
254
|
-
// return {}; // strip circular refs
|
|
255
|
-
// }
|
|
256
|
-
// }
|
|
257
|
-
|
|
258
|
-
// private pick(obj: any, keys: string[]) {
|
|
259
|
-
// const out: any = {};
|
|
260
|
-
// for (const k of keys) out[k] = obj?.[k];
|
|
261
|
-
// return this.safeCopy(out);
|
|
262
|
-
// }
|
|
263
|
-
|
|
264
|
-
// private getUserId(): string | null {
|
|
265
|
-
|
|
266
|
-
// const activeUser = this.requestContext.getActiveUser();
|
|
267
|
-
// if (activeUser?.sub)
|
|
268
|
-
// return String(activeUser.sub);
|
|
269
|
-
// }
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// }
|