@solidxai/core 0.1.10-alpha.1 → 0.1.10-beta.10

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 (46) hide show
  1. package/dist/dtos/update-user.dto.d.ts +1 -0
  2. package/dist/dtos/update-user.dto.d.ts.map +1 -1
  3. package/dist/dtos/update-user.dto.js +7 -1
  4. package/dist/dtos/update-user.dto.js.map +1 -1
  5. package/dist/entities/user.entity.js +1 -0
  6. package/dist/entities/user.entity.js.map +1 -1
  7. package/dist/jobs/database/chatter-queue-subscriber-database.service.d.ts.map +1 -1
  8. package/dist/jobs/database/chatter-queue-subscriber-database.service.js +3 -3
  9. package/dist/jobs/database/chatter-queue-subscriber-database.service.js.map +1 -1
  10. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.d.ts.map +1 -1
  11. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js +3 -3
  12. package/dist/jobs/rabbitmq/chatter-queue-subscriber.service.js.map +1 -1
  13. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.d.ts.map +1 -1
  14. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js +3 -3
  15. package/dist/jobs/redis/chatter-queue-subscriber-redis.service.js.map +1 -1
  16. package/dist/seeders/module-test-data.service.d.ts +7 -0
  17. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  18. package/dist/seeders/module-test-data.service.js +94 -18
  19. package/dist/seeders/module-test-data.service.js.map +1 -1
  20. package/dist/seeders/seed-data/solid-core-metadata.json +21 -0
  21. package/dist/services/chatter-message.service.d.ts +6 -3
  22. package/dist/services/chatter-message.service.d.ts.map +1 -1
  23. package/dist/services/chatter-message.service.js +23 -35
  24. package/dist/services/chatter-message.service.js.map +1 -1
  25. package/dist/testing/reporter/console-reporter.d.ts +10 -0
  26. package/dist/testing/reporter/console-reporter.d.ts.map +1 -1
  27. package/dist/testing/reporter/console-reporter.js +21 -0
  28. package/dist/testing/reporter/console-reporter.js.map +1 -1
  29. package/dist/testing/reporter/reporter.types.d.ts +7 -0
  30. package/dist/testing/reporter/reporter.types.d.ts.map +1 -1
  31. package/dist/testing/reporter/reporter.types.js.map +1 -1
  32. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -1
  33. package/dist/testing/runner/run-from-metadata.js +20 -1
  34. package/dist/testing/runner/run-from-metadata.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/dtos/update-user.dto.ts +4 -0
  37. package/src/entities/user.entity.ts +1 -1
  38. package/src/jobs/database/chatter-queue-subscriber-database.service.ts +4 -2
  39. package/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +4 -2
  40. package/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +10 -3
  41. package/src/seeders/module-test-data.service.ts +107 -15
  42. package/src/seeders/seed-data/solid-core-metadata.json +21 -0
  43. package/src/services/chatter-message.service.ts +27 -37
  44. package/src/testing/reporter/console-reporter.ts +27 -0
  45. package/src/testing/reporter/reporter.types.ts +7 -0
  46. package/src/testing/runner/run-from-metadata.ts +19 -1
@@ -33,7 +33,7 @@ export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber<AuditQueu
33
33
 
34
34
  switch (p.eventType) {
35
35
  case 'insert':
36
- await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName);
36
+ await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName, false, p.userId);
37
37
  break;
38
38
  case 'update':
39
39
  await this.chatterMessageService.postAuditMessageOnUpdate(
@@ -41,10 +41,12 @@ export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber<AuditQueu
41
41
  p.modelName,
42
42
  p.before,
43
43
  (p.updatedColumnNames ?? []).map(n => ({ propertyName: n })),
44
+ false,
45
+ p.userId,
44
46
  );
45
47
  break;
46
48
  case 'delete':
47
- await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before);
49
+ await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId);
48
50
  break;
49
51
  }
50
52
  }
@@ -33,13 +33,20 @@ export class ChatterQueueSubscriberRedis extends RedisSubscriber<any> {
33
33
 
34
34
  switch (p.eventType) {
35
35
  case 'insert':
36
- await this.chatterMessageService.postAuditMessageOnInsert(p.after, { name: p.modelName } as any);
36
+ await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName, false, p.userId);
37
37
  break;
38
38
  case 'update':
39
- await this.chatterMessageService.postAuditMessageOnUpdate(p.after, { name: p.modelName } as any, p.before, (p.updatedColumnNames || []).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
+ false,
45
+ p.userId,
46
+ );
40
47
  break;
41
48
  case 'delete':
42
- await this.chatterMessageService.postAuditMessageOnDelete(p.before, { name: p.modelName } as any, p.before);
49
+ await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId);
43
50
  break;
44
51
  }
45
52
  }
@@ -24,6 +24,7 @@ import { TestingRoleSpec, TestingUserSpec } from 'src/testing/contracts/testing-
24
24
  @Injectable()
25
25
  export class ModuleTestDataService {
26
26
  private readonly logger = new Logger(ModuleTestDataService.name);
27
+ private static readonly TEARDOWN_RETRY_ATTEMPTS = 5;
27
28
 
28
29
  constructor(
29
30
  private readonly moduleRef: ModuleRef,
@@ -665,31 +666,122 @@ export class ModuleTestDataService {
665
666
  private async dropTestDatabaseObjects(databases: Record<string, string>): Promise<void> {
666
667
  const entries = Object.entries(databases);
667
668
  for (const [dsName, dbName] of entries) {
668
- const dataSource = this.resolveDataSourceByName(dsName);
669
- if (!dataSource.isInitialized) {
670
- await dataSource.initialize();
671
- }
669
+ await this.dropTestDatabaseObjectsWithRetry(dsName, dbName);
670
+ }
671
+ }
672
672
 
673
- console.log(`Dropping test database/schema "${dbName}" on datasource "${dsName}"...`);
673
+ private async dropTestDatabaseObjectsWithRetry(dsName: string, dbName: string): Promise<void> {
674
+ let lastError: unknown;
675
+
676
+ for (let attempt = 1; attempt <= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS; attempt += 1) {
677
+ console.log(`Attempting to tear down "${dbName}" on datasource "${dsName}" (${attempt}/${ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS})...`);
674
678
 
675
- const queryRunner = dataSource.createQueryRunner();
676
679
  try {
677
- const type = dataSource.options.type;
678
- if (type === 'postgres') {
679
- await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`);
680
- } else if (type === 'mssql') {
681
- await this.dropMssqlSchema(queryRunner, dbName);
682
- } else if (type === 'mysql' || type === 'mariadb') {
683
- await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
684
- } else {
685
- throw new Error(`Unsupported database type for test data deletion: ${type}`);
680
+ await this.dropSingleTestDatabaseObject(dsName, dbName);
681
+ return;
682
+ } catch (error) {
683
+ lastError = error;
684
+ if (attempt >= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS) {
685
+ throw error;
686
686
  }
687
+
688
+ await this.sleep(this.teardownRetryDelayMs(attempt));
689
+ }
690
+ }
691
+
692
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
693
+ }
694
+
695
+ private teardownRetryDelayMs(attempt: number): number {
696
+ const baseMs = 500;
697
+ const incrementMs = 350;
698
+ const jitterMs = Math.floor(Math.random() * 250);
699
+ return baseMs + ((attempt - 1) * incrementMs) + jitterMs;
700
+ }
701
+
702
+ private async sleep(ms: number): Promise<void> {
703
+ await new Promise((resolve) => setTimeout(resolve, ms));
704
+ }
705
+
706
+ private async dropSingleTestDatabaseObject(dsName: string, dbName: string): Promise<void> {
707
+ const dataSource = this.resolveDataSourceByName(dsName);
708
+
709
+ if (dataSource.options.type === 'postgres') {
710
+ await this.dropPostgresDatabase(dataSource, dbName);
711
+ return;
712
+ }
713
+
714
+ if (!dataSource.isInitialized) {
715
+ await dataSource.initialize();
716
+ }
717
+
718
+ const queryRunner = dataSource.createQueryRunner();
719
+ try {
720
+ const type = dataSource.options.type;
721
+ if (type === 'mssql') {
722
+ await this.dropMssqlSchema(queryRunner, dbName);
723
+ } else if (type === 'mysql' || type === 'mariadb') {
724
+ await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``);
725
+ } else {
726
+ throw new Error(`Unsupported database type for test data deletion: ${type}`);
727
+ }
728
+ } finally {
729
+ await queryRunner.release();
730
+ }
731
+ }
732
+
733
+ private async dropPostgresDatabase(dataSource: DataSource, dbName: string): Promise<void> {
734
+ if (dataSource.isInitialized) {
735
+ await dataSource.destroy();
736
+ }
737
+
738
+ const adminDataSource = new DataSource({
739
+ ...(dataSource.options as any),
740
+ database: this.resolvePostgresMaintenanceDatabase(dataSource),
741
+ name: `${String(dataSource.name ?? 'default')}_teardown_admin_${Date.now()}`,
742
+ synchronize: false,
743
+ migrationsRun: false,
744
+ entities: [],
745
+ subscribers: [],
746
+ migrations: [],
747
+ });
748
+
749
+ try {
750
+ await adminDataSource.initialize();
751
+ const queryRunner = adminDataSource.createQueryRunner();
752
+ try {
753
+ await queryRunner.query(
754
+ `SELECT pg_terminate_backend(pid)
755
+ FROM pg_stat_activity
756
+ WHERE datname = $1
757
+ AND pid <> pg_backend_pid()`,
758
+ [dbName],
759
+ );
760
+ await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`);
687
761
  } finally {
688
762
  await queryRunner.release();
689
763
  }
764
+ } finally {
765
+ if (adminDataSource.isInitialized) {
766
+ await adminDataSource.destroy();
767
+ }
690
768
  }
691
769
  }
692
770
 
771
+ private resolvePostgresMaintenanceDatabase(dataSource: DataSource): string {
772
+ const configured = process.env.POSTGRES_MAINTENANCE_DATABASE?.trim();
773
+ if (configured) {
774
+ return configured;
775
+ }
776
+
777
+ const currentDb = String((dataSource.options as any)?.database ?? '').trim();
778
+ if (currentDb && currentDb !== 'postgres') {
779
+ return 'postgres';
780
+ }
781
+
782
+ return 'template1';
783
+ }
784
+
693
785
  private async dropMssqlSchema(queryRunner: ReturnType<DataSource['createQueryRunner']>, schemaName: string): Promise<void> {
694
786
  const foreignKeys: Array<{ fk_name: string; table_name: string }> = await queryRunner.query(
695
787
  `SELECT fk.name AS fk_name, t.name AS table_name
@@ -1761,6 +1761,19 @@
1761
1761
  "encrypt": false,
1762
1762
  "isSystem": true
1763
1763
  },
1764
+ {
1765
+ "name": "failedLoginAttempts",
1766
+ "displayName": "Failed Login Attempts",
1767
+ "type": "int",
1768
+ "ormType": "integer",
1769
+ "defaultValue": "0",
1770
+ "required": false,
1771
+ "unique": false,
1772
+ "index": false,
1773
+ "private": false,
1774
+ "encrypt": false,
1775
+ "isSystem": true
1776
+ },
1764
1777
  {
1765
1778
  "name": "profilePicture",
1766
1779
  "displayName": "Profile Picture",
@@ -9841,6 +9854,14 @@
9841
9854
  "name": "active",
9842
9855
  "isSearchable": true
9843
9856
  }
9857
+ },
9858
+ {
9859
+ "type": "field",
9860
+ "attrs": {
9861
+ "label" : "Blocked / Unblocked",
9862
+ "name": "failedLoginAttempts",
9863
+ "viewWidget": "SolidUserBlockedStatusListWidget"
9864
+ }
9844
9865
  }
9845
9866
  ]
9846
9867
  }
@@ -47,6 +47,26 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
47
47
  super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef);
48
48
  }
49
49
 
50
+ private resolveMessageUserId(userId?: number | null): number | null {
51
+ if (userId) {
52
+ return userId;
53
+ }
54
+
55
+ return this.requestContextService.getActiveUser()?.sub ?? null;
56
+ }
57
+
58
+ private resolveMessageUser(userId?: number | null) {
59
+ const resolvedUserId = this.resolveMessageUserId(userId);
60
+ return resolvedUserId ? ({ id: resolvedUserId } as any) : null;
61
+ }
62
+
63
+ private stampMessageAuditFields(chatterMessage: ChatterMessage, userId?: number | null) {
64
+ const resolvedUserId = this.resolveMessageUserId(userId);
65
+ chatterMessage.user = resolvedUserId ? ({ id: resolvedUserId } as any) : null;
66
+ chatterMessage.createdBy = resolvedUserId;
67
+ chatterMessage.updatedBy = resolvedUserId;
68
+ }
69
+
50
70
  async markCompleted(id: number) {
51
71
  const activeUser = this.requestContextService.getActiveUser();
52
72
  if (!activeUser) {
@@ -78,14 +98,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
78
98
  });
79
99
  chatterMessage.modelDisplayName = model?.displayName ?? null;
80
100
 
81
- const activeUser = this.requestContextService.getActiveUser();
82
-
83
- if (activeUser) {
84
- const userId = activeUser?.sub;
85
- chatterMessage.user = { id: userId } as any;
86
- } else {
87
- chatterMessage.user = null;
88
- }
101
+ this.stampMessageAuditFields(chatterMessage);
89
102
 
90
103
  const savedMessage = await this.repo.save(chatterMessage);
91
104
 
@@ -114,7 +127,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
114
127
  return savedMessage;
115
128
  }
116
129
 
117
- async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false) {
130
+ async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false, userId?: number | null) {
118
131
  if (!entity) {
119
132
  return;
120
133
  }
@@ -139,8 +152,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
139
152
  !(field.type === 'relation' && field.relationType === 'one-to-many')
140
153
  );
141
154
 
142
- const activeUser = this.requestContextService.getActiveUser();
143
-
144
155
  const chatterMessage = new ChatterMessage();
145
156
  chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT;
146
157
  chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_INSERT;
@@ -150,13 +161,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
150
161
  chatterMessage.modelDisplayName = model?.displayName;
151
162
  chatterMessage.modelUserKey = entity[model?.userKeyField?.name];
152
163
  chatterMessage.messageBody = `New ${model?.displayName} created`;
153
-
154
- if (activeUser) {
155
- const userId = activeUser?.sub;
156
- chatterMessage.user = { id: userId } as any;
157
- } else {
158
- chatterMessage.user = null;
159
- }
164
+ this.stampMessageAuditFields(chatterMessage, userId);
160
165
 
161
166
  const savedMessage = await this.repo.save(chatterMessage);
162
167
 
@@ -177,7 +182,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
177
182
  }
178
183
  }
179
184
 
180
- async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false) {
185
+ async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false, userId?: number | null) {
181
186
  if (!databaseEntity || !entity) {
182
187
  return;
183
188
  }
@@ -259,8 +264,6 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
259
264
  return;
260
265
  }
261
266
 
262
- const activeUser = this.requestContextService.getActiveUser();
263
-
264
267
  const chatterMessage = new ChatterMessage();
265
268
  chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT;
266
269
  chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_UPDATE;
@@ -270,13 +273,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
270
273
  chatterMessage.modelDisplayName = model.displayName;
271
274
  chatterMessage.modelUserKey = entity[model?.userKeyField?.name];
272
275
  chatterMessage.messageBody = `${model?.displayName} updated`;
273
-
274
- if (activeUser) {
275
- const userId = activeUser?.sub;
276
- chatterMessage.user = { id: userId } as any;
277
- } else {
278
- chatterMessage.user = null;
279
- }
276
+ this.stampMessageAuditFields(chatterMessage, userId);
280
277
 
281
278
  const savedMessage = await this.repo.save(chatterMessage);
282
279
 
@@ -294,7 +291,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
294
291
  }
295
292
  }
296
293
 
297
- async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false) {
294
+ async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false, userId?: number | null) {
298
295
  const model = await this.modelMetadataRepo.findOne({
299
296
  where: {
300
297
  singularName: lowerFirst(modelName)
@@ -335,14 +332,7 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
335
332
  chatterMessage.modelUserKey = databaseEntity[model?.userKeyField?.name];
336
333
  chatterMessage.messageBody = `${model?.displayName} deleted`;
337
334
 
338
- const activeUser = this.requestContextService.getActiveUser();
339
-
340
- if (activeUser) {
341
- const userId = activeUser?.sub;
342
- chatterMessage.user = { id: userId } as any;
343
- } else {
344
- chatterMessage.user = null;
345
- }
335
+ this.stampMessageAuditFields(chatterMessage, userId);
346
336
 
347
337
  const savedMessage = await this.repo.save(chatterMessage);
348
338
 
@@ -146,6 +146,10 @@ function formatStepLabel(step: OpStep): string {
146
146
  }
147
147
 
148
148
  export class ConsoleReporter implements Reporter {
149
+ private totalScenarios = 0;
150
+ private passedScenarios = 0;
151
+ private failedScenarios = 0;
152
+
149
153
  onScenarioStart(scenario: { id: string; name?: string }): void {
150
154
  const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
151
155
  console.log(`\n▶ Scenario: ${label}`);
@@ -157,6 +161,12 @@ export class ConsoleReporter implements Reporter {
157
161
  ): void {
158
162
  const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id;
159
163
  const status = result.ok ? "✔" : "✖";
164
+ this.totalScenarios += 1;
165
+ if (result.ok) {
166
+ this.passedScenarios += 1;
167
+ } else {
168
+ this.failedScenarios += 1;
169
+ }
160
170
  console.log(`${status} Scenario: ${label} (${result.durationMs}ms)`);
161
171
  }
162
172
 
@@ -226,4 +236,21 @@ export class ConsoleReporter implements Reporter {
226
236
  if (!dataText.length) return;
227
237
  console.log(indentLines(dataText, `${STEP_INDENT}${INDENT}${INDENT}`));
228
238
  }
239
+
240
+ onRunEnd(args: {
241
+ ok: boolean;
242
+ total: number;
243
+ passed: number;
244
+ failed: number;
245
+ durationMs: number;
246
+ }): void {
247
+ const durationSeconds = (args.durationMs / 1000).toFixed(2);
248
+ const finalStatus = args.ok ? "PASSED" : "FAILED";
249
+
250
+ console.log("\n════════ Test Run Summary ════════");
251
+ console.log(`Result: Test run ${finalStatus}`);
252
+ console.log(`Cases: total=${args.total}, passed=${args.passed}, failed=${args.failed}`);
253
+ console.log(`Duration: ${durationSeconds}s`);
254
+ console.log("══════════════════════════════════");
255
+ }
229
256
  }
@@ -33,4 +33,11 @@ export interface Reporter {
33
33
  contentType: string;
34
34
  data: Buffer | string;
35
35
  }): void;
36
+ onRunEnd?(args: {
37
+ ok: boolean;
38
+ total: number;
39
+ passed: number;
40
+ failed: number;
41
+ durationMs: number;
42
+ }): void;
36
43
  }
@@ -44,6 +44,7 @@ export type RunnerOptions = {
44
44
  };
45
45
 
46
46
  export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
47
+ const startedAt = Date.now();
47
48
  const registry = new StepRegistry();
48
49
  registerApiSteps(registry);
49
50
  registerUiSteps(registry);
@@ -71,15 +72,32 @@ export async function runFromMetadata(opts: RunnerOptions): Promise<void> {
71
72
  const ui = new PlaywrightAdapter(opts.ui);
72
73
  const ctxBase = { resources, reporter, api, ui, specRegistry, testData, options: opts.options };
73
74
  const uiStarted = { value: false };
75
+ let passed = 0;
76
+ let failed = 0;
77
+ let runError: unknown;
74
78
 
75
79
  try {
76
80
  for (const scenario of scenarios) {
77
81
  if (scenarioNeedsUi(scenario)) {
78
82
  await ensureUiStarted(ctxBase, uiStarted);
79
83
  }
80
- await engine.runScenario(scenario, ctxBase);
84
+ try {
85
+ await engine.runScenario(scenario, ctxBase);
86
+ passed += 1;
87
+ } catch (error) {
88
+ failed += 1;
89
+ runError = error;
90
+ throw error;
91
+ }
81
92
  }
82
93
  } finally {
94
+ reporter.onRunEnd?.({
95
+ ok: !runError,
96
+ total: scenarios.length,
97
+ passed,
98
+ failed,
99
+ durationMs: Date.now() - startedAt,
100
+ });
83
101
  if (uiStarted.value) {
84
102
  await ui.stop();
85
103
  }