@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.
Files changed (62) hide show
  1. package/.claude/settings.local.json +15 -0
  2. package/dist/entities/mq-message-queue.entity.d.ts.map +1 -1
  3. package/dist/entities/mq-message-queue.entity.js.map +1 -1
  4. package/dist/helpers/cors.helper.d.ts.map +1 -1
  5. package/dist/helpers/cors.helper.js +8 -1
  6. package/dist/helpers/cors.helper.js.map +1 -1
  7. package/dist/jobs/chatter-queue-options.js +1 -1
  8. package/dist/jobs/chatter-queue-options.js.map +1 -1
  9. package/dist/jobs/chatter-queue-publisher.service.d.ts +9 -9
  10. package/dist/jobs/chatter-queue-publisher.service.d.ts.map +1 -1
  11. package/dist/jobs/chatter-queue-publisher.service.js +5 -5
  12. package/dist/jobs/chatter-queue-publisher.service.js.map +1 -1
  13. package/dist/jobs/chatter-queue-subscriber.service.d.ts +4 -4
  14. package/dist/jobs/chatter-queue-subscriber.service.d.ts.map +1 -1
  15. package/dist/jobs/chatter-queue-subscriber.service.js +11 -11
  16. package/dist/jobs/chatter-queue-subscriber.service.js.map +1 -1
  17. package/dist/jobs/database/chatter-queue-options-database.d.ts +8 -0
  18. package/dist/jobs/database/chatter-queue-options-database.d.ts.map +1 -0
  19. package/dist/jobs/database/chatter-queue-options-database.js +10 -0
  20. package/dist/jobs/database/chatter-queue-options-database.js.map +1 -0
  21. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts +12 -0
  22. package/dist/jobs/database/chatter-queue-publisher-database.service.d.ts.map +1 -0
  23. package/dist/jobs/database/chatter-queue-publisher-database.service.js +39 -0
  24. package/dist/jobs/database/chatter-queue-publisher-database.service.js.map +1 -0
  25. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts +19 -0
  26. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -0
  27. package/dist/jobs/database/chatter-queue-subscriber-database.service.js +62 -0
  28. package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -0
  29. package/dist/services/chatter-message.service.d.ts +4 -4
  30. package/dist/services/chatter-message.service.d.ts.map +1 -1
  31. package/dist/services/chatter-message.service.js +33 -9
  32. package/dist/services/chatter-message.service.js.map +1 -1
  33. package/dist/solid-core.module.d.ts.map +1 -1
  34. package/dist/solid-core.module.js +8 -0
  35. package/dist/solid-core.module.js.map +1 -1
  36. package/dist/subscribers/audit.subscriber.d.ts +8 -3
  37. package/dist/subscribers/audit.subscriber.d.ts.map +1 -1
  38. package/dist/subscribers/audit.subscriber.js +54 -52
  39. package/dist/subscribers/audit.subscriber.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/entities/mq-message-queue.entity.ts +8 -8
  42. package/src/helpers/cors.helper.ts +14 -2
  43. package/src/jobs/chatter-queue-options.ts +1 -1
  44. package/src/jobs/chatter-queue-publisher.service.ts +11 -11
  45. package/src/jobs/chatter-queue-subscriber.service.ts +13 -8
  46. package/src/jobs/database/chatter-queue-options-database.ts +9 -0
  47. package/src/jobs/database/chatter-queue-publisher-database.service.ts +24 -0
  48. package/src/jobs/database/chatter-queue-subscriber-database.service.ts +53 -0
  49. package/src/services/1.js +6 -0
  50. package/src/services/chatter-message.service.ts +41 -9
  51. package/src/solid-core.module.ts +8 -2
  52. package/src/subscribers/audit.subscriber.ts +59 -224
  53. package/dist-tests/api/authenticate.spec.js +0 -119
  54. package/dist-tests/api/authenticate.spec.js.map +0 -1
  55. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +0 -97
  56. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +0 -1
  57. package/dist-tests/api/ping.spec.js +0 -21
  58. package/dist-tests/api/ping.spec.js.map +0 -1
  59. package/dist-tests/helpers/auth.js +0 -41
  60. package/dist-tests/helpers/auth.js.map +0 -1
  61. package/dist-tests/helpers/env.js +0 -11
  62. 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 ChatterEventType = 'insert' | 'update' | 'delete';
10
+ export type AuditEventType = 'insert' | 'update' | 'delete';
11
11
 
12
- export interface ChatterMessagePayload {
13
- eventType: ChatterEventType;
14
- model: string; // entity name
15
- entityId: string; // id string
16
- occurredAt: string; // ISO
17
- before?: any;
18
- after?: any;
19
- diff?: string[]; // changed column names for updates
20
- userId?: string | null;
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 ChatterQueuePublisher extends RabbitMqPublisher<any> {
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 { ChatterMessagePayload } from './chatter-queue-publisher.service';
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 ChatterQueueSubscriber extends RabbitMqSubscriber<any> {
14
- private readonly chatterQueueLogger = new Logger(ChatterQueueSubscriber.name);
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<ChatterMessagePayload>) {
30
+ async subscribe(message: QueueMessage<AuditQueuePayload>) {
31
31
  const p = message.payload;
32
- this.chatterQueueLogger.debug(`Audit event ${p.eventType} ${p.model}#${p.entityId}`);
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, { name: p.model } as any);
36
+ await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName);
37
37
  break;
38
38
  case 'update':
39
- await this.chatterMessageService.postAuditMessageOnUpdate(p.after, { name: p.model } as any, p.before, (p.diff || []).map(n => ({ propertyName: n })));
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.before, { name: p.model } as any, p.before);
47
+ await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before);
43
48
  break;
44
49
  }
45
50
  }
@@ -0,0 +1,9 @@
1
+ import { BrokerType } from "../../interfaces";
2
+
3
+ const QUEUE_NAME = 'solid_chatter_queue_database';
4
+
5
+ export default {
6
+ name: QUEUE_NAME,
7
+ type: BrokerType.Database,
8
+ queueName: QUEUE_NAME,
9
+ };
@@ -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, metadata: EntityMetadata, messageQueue: boolean = false) {
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(metadata.name)
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, metadata: EntityMetadata, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false) {
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(metadata.name)
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(entity: any, metadata: EntityMetadata, databaseEntity: any, messageQueue: boolean = false) {
272
+ async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false) {
272
273
  const model = await this.modelMetadataRepo.findOne({
273
274
  where: {
274
- singularName: lowerFirst(metadata.name)
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 = entity[model?.userKeyField?.name];
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 {
@@ -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
- // ChatterQueuePublisher,
599
- // ChatterQueueSubscriber,
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 { ChatterMessageService } from '../services/chatter-message.service';
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 chatterMessageService: ChatterMessageService,
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, DeferredCall[]>();
25
+ private perTxn = new WeakMap<any, AuditQueuePayload[]>();
29
26
 
30
- private enqueue(event: { queryRunner: any }, call: DeferredCall) {
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(call);
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
- // await this.chatterMessageService.postAuditMessageOnInsert(event.entity, event.metadata);
44
- this.enqueue(event, {
45
- kind: 'insert',
46
- args: [event.entity, event.metadata] as Parameters<ChatterMessageService['postAuditMessageOnInsert']>,
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
- // await this.chatterMessageService.postAuditMessageOnUpdate(event.entity, event.metadata, event.databaseEntity, event.updatedColumns || []);
54
- this.enqueue(event, {
55
- kind: 'update',
56
- args: [
57
- event.entity, // entity (after)
58
- event.metadata,
59
- event.databaseEntity, // entity (before)
60
- event.updatedColumns ?? [],
61
- ] as Parameters<ChatterMessageService['postAuditMessageOnUpdate']>,
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
- // await this.chatterMessageService.postAuditMessageOnDelete(event.entity, event.metadata, event.databaseEntity);
69
- this.enqueue(event, {
70
- kind: 'delete',
71
- args: [
72
- event.entity,
73
- event.metadata,
74
- event.databaseEntity,
75
- ] as Parameters<ChatterMessageService['postAuditMessageOnDelete']>,
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 we’re OUTSIDE the DB transaction — safe to do I/O/DB writes inside chatter service.
86
- for (const item of batch) {
87
- try {
88
- switch (item.kind) {
89
- case 'insert': await this.chatterMessageService.postAuditMessageOnInsert(...item.args); break;
90
- case 'update': await this.chatterMessageService.postAuditMessageOnUpdate(...item.args); break;
91
- case 'delete': await this.chatterMessageService.postAuditMessageOnDelete(...item.args); break;
92
- }
93
- } catch (e) {
94
- // Best effort: log and continue; your core txn was already committed
95
- // Optionally: send to a generic error logger/metric here
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 calls; the write never happened
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
- // }