@solidxai/core 0.1.1 → 0.1.4

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 (295) hide show
  1. package/dist/commands/run-tests.command.d.ts +37 -0
  2. package/dist/commands/run-tests.command.d.ts.map +1 -0
  3. package/dist/commands/run-tests.command.js +345 -0
  4. package/dist/commands/run-tests.command.js.map +1 -0
  5. package/dist/commands/test-data.command.d.ts +6 -6
  6. package/dist/commands/test-data.command.d.ts.map +1 -1
  7. package/dist/commands/test-data.command.js +25 -25
  8. package/dist/commands/test-data.command.js.map +1 -1
  9. package/dist/commands/test.command.d.ts +5 -0
  10. package/dist/commands/test.command.d.ts.map +1 -0
  11. package/dist/commands/test.command.js +26 -0
  12. package/dist/commands/test.command.js.map +1 -0
  13. package/dist/controllers/service.controller.d.ts +0 -9
  14. package/dist/controllers/service.controller.d.ts.map +1 -1
  15. package/dist/controllers/service.controller.js +0 -45
  16. package/dist/controllers/service.controller.js.map +1 -1
  17. package/dist/dtos/basic-filters.dto.d.ts.map +1 -1
  18. package/dist/dtos/basic-filters.dto.js.map +1 -1
  19. package/dist/dtos/create-user.dto.d.ts +1 -0
  20. package/dist/dtos/create-user.dto.d.ts.map +1 -1
  21. package/dist/dtos/create-user.dto.js +2 -1
  22. package/dist/dtos/create-user.dto.js.map +1 -1
  23. package/dist/helpers/schematic.service.js +1 -1
  24. package/dist/helpers/schematic.service.js.map +1 -1
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  30. package/dist/seeders/module-metadata-seeder.service.js +3 -21
  31. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  32. package/dist/seeders/module-test-data.service.d.ts.map +1 -1
  33. package/dist/seeders/module-test-data.service.js +3 -3
  34. package/dist/seeders/module-test-data.service.js.map +1 -1
  35. package/dist/seeders/seed-data/solid-core-metadata.json +34 -9
  36. package/dist/services/chatter-message.service.d.ts +2 -0
  37. package/dist/services/chatter-message.service.d.ts.map +1 -1
  38. package/dist/services/chatter-message.service.js +18 -2
  39. package/dist/services/chatter-message.service.js.map +1 -1
  40. package/dist/services/crud.service.d.ts.map +1 -1
  41. package/dist/services/crud.service.js.map +1 -1
  42. package/dist/services/model-metadata.service.d.ts.map +1 -1
  43. package/dist/services/model-metadata.service.js +2 -1
  44. package/dist/services/model-metadata.service.js.map +1 -1
  45. package/dist/services/module-metadata.service.d.ts.map +1 -1
  46. package/dist/services/module-metadata.service.js +2 -1
  47. package/dist/services/module-metadata.service.js.map +1 -1
  48. package/dist/services/queues/common.d.ts +3 -0
  49. package/dist/services/queues/common.d.ts.map +1 -0
  50. package/dist/services/queues/common.js +39 -0
  51. package/dist/services/queues/common.js.map +1 -0
  52. package/dist/services/queues/database-publisher.service.d.ts.map +1 -1
  53. package/dist/services/queues/database-publisher.service.js +3 -1
  54. package/dist/services/queues/database-publisher.service.js.map +1 -1
  55. package/dist/services/queues/database-subscriber.service.d.ts.map +1 -1
  56. package/dist/services/queues/database-subscriber.service.js +5 -2
  57. package/dist/services/queues/database-subscriber.service.js.map +1 -1
  58. package/dist/services/queues/rabbitmq-publisher.service.d.ts.map +1 -1
  59. package/dist/services/queues/rabbitmq-publisher.service.js +13 -6
  60. package/dist/services/queues/rabbitmq-publisher.service.js.map +1 -1
  61. package/dist/services/queues/rabbitmq-subscriber.service.d.ts +14 -1
  62. package/dist/services/queues/rabbitmq-subscriber.service.d.ts.map +1 -1
  63. package/dist/services/queues/rabbitmq-subscriber.service.js +197 -65
  64. package/dist/services/queues/rabbitmq-subscriber.service.js.map +1 -1
  65. package/dist/solid-core.module.d.ts.map +1 -1
  66. package/dist/solid-core.module.js +4 -0
  67. package/dist/solid-core.module.js.map +1 -1
  68. package/dist/testing/__examples__/register-example-specs.d.ts +3 -0
  69. package/dist/testing/__examples__/register-example-specs.d.ts.map +1 -0
  70. package/dist/testing/__examples__/register-example-specs.js +8 -0
  71. package/dist/testing/__examples__/register-example-specs.js.map +1 -0
  72. package/dist/testing/__examples__/specs/custom-health.spec.d.ts +17 -0
  73. package/dist/testing/__examples__/specs/custom-health.spec.d.ts.map +1 -0
  74. package/dist/testing/__examples__/specs/custom-health.spec.js +30 -0
  75. package/dist/testing/__examples__/specs/custom-health.spec.js.map +1 -0
  76. package/dist/testing/adapters/api/api-adapter.d.ts +9 -0
  77. package/dist/testing/adapters/api/api-adapter.d.ts.map +1 -0
  78. package/dist/testing/adapters/api/api-adapter.js +76 -0
  79. package/dist/testing/adapters/api/api-adapter.js.map +1 -0
  80. package/dist/testing/adapters/api/api.types.d.ts +14 -0
  81. package/dist/testing/adapters/api/api.types.d.ts.map +1 -0
  82. package/dist/testing/adapters/api/api.types.js +3 -0
  83. package/dist/testing/adapters/api/api.types.js.map +1 -0
  84. package/dist/testing/adapters/ui/playwright-adapter.d.ts +14 -0
  85. package/dist/testing/adapters/ui/playwright-adapter.d.ts.map +1 -0
  86. package/dist/testing/adapters/ui/playwright-adapter.js +47 -0
  87. package/dist/testing/adapters/ui/playwright-adapter.js.map +1 -0
  88. package/dist/testing/adapters/ui/ui.types.d.ts +5 -0
  89. package/dist/testing/adapters/ui/ui.types.d.ts.map +1 -0
  90. package/dist/testing/adapters/ui/ui.types.js +3 -0
  91. package/dist/testing/adapters/ui/ui.types.js.map +1 -0
  92. package/dist/testing/contracts/runtime-context.types.d.ts +35 -0
  93. package/dist/testing/contracts/runtime-context.types.d.ts.map +1 -0
  94. package/dist/testing/contracts/runtime-context.types.js +3 -0
  95. package/dist/testing/contracts/runtime-context.types.js.map +1 -0
  96. package/dist/testing/contracts/test-spec.types.d.ts +21 -0
  97. package/dist/testing/contracts/test-spec.types.d.ts.map +1 -0
  98. package/dist/testing/contracts/test-spec.types.js +3 -0
  99. package/dist/testing/contracts/test-spec.types.js.map +1 -0
  100. package/dist/testing/contracts/testing-metadata.types.d.ts +41 -0
  101. package/dist/testing/contracts/testing-metadata.types.d.ts.map +1 -0
  102. package/dist/testing/contracts/testing-metadata.types.js +3 -0
  103. package/dist/testing/contracts/testing-metadata.types.js.map +1 -0
  104. package/dist/testing/core/interpolation.d.ts +4 -0
  105. package/dist/testing/core/interpolation.d.ts.map +1 -0
  106. package/dist/testing/core/interpolation.js +180 -0
  107. package/dist/testing/core/interpolation.js.map +1 -0
  108. package/dist/testing/core/normalize-steps.d.ts +7 -0
  109. package/dist/testing/core/normalize-steps.d.ts.map +1 -0
  110. package/dist/testing/core/normalize-steps.js +20 -0
  111. package/dist/testing/core/normalize-steps.js.map +1 -0
  112. package/dist/testing/core/resource-store.d.ts +8 -0
  113. package/dist/testing/core/resource-store.d.ts.map +1 -0
  114. package/dist/testing/core/resource-store.js +41 -0
  115. package/dist/testing/core/resource-store.js.map +1 -0
  116. package/dist/testing/core/spec-registry.d.ts +10 -0
  117. package/dist/testing/core/spec-registry.d.ts.map +1 -0
  118. package/dist/testing/core/spec-registry.js +32 -0
  119. package/dist/testing/core/spec-registry.js.map +1 -0
  120. package/dist/testing/core/step-registry.d.ts +10 -0
  121. package/dist/testing/core/step-registry.d.ts.map +1 -0
  122. package/dist/testing/core/step-registry.js +26 -0
  123. package/dist/testing/core/step-registry.js.map +1 -0
  124. package/dist/testing/core/testing-engine.d.ts +14 -0
  125. package/dist/testing/core/testing-engine.d.ts.map +1 -0
  126. package/dist/testing/core/testing-engine.js +97 -0
  127. package/dist/testing/core/testing-engine.js.map +1 -0
  128. package/dist/testing/core/timeout.d.ts +2 -0
  129. package/dist/testing/core/timeout.d.ts.map +1 -0
  130. package/dist/testing/core/timeout.js +18 -0
  131. package/dist/testing/core/timeout.js.map +1 -0
  132. package/dist/testing/reporter/attachments.d.ts +4 -0
  133. package/dist/testing/reporter/attachments.d.ts.map +1 -0
  134. package/dist/testing/reporter/attachments.js +25 -0
  135. package/dist/testing/reporter/attachments.js.map +1 -0
  136. package/dist/testing/reporter/console-reporter.d.ts +45 -0
  137. package/dist/testing/reporter/console-reporter.d.ts.map +1 -0
  138. package/dist/testing/reporter/console-reporter.js +189 -0
  139. package/dist/testing/reporter/console-reporter.js.map +1 -0
  140. package/dist/testing/reporter/reporter.types.d.ts +37 -0
  141. package/dist/testing/reporter/reporter.types.d.ts.map +1 -0
  142. package/dist/testing/reporter/reporter.types.js +3 -0
  143. package/dist/testing/reporter/reporter.types.js.map +1 -0
  144. package/dist/testing/runner/lifecycle.d.ts +9 -0
  145. package/dist/testing/runner/lifecycle.d.ts.map +1 -0
  146. package/dist/testing/runner/lifecycle.js +33 -0
  147. package/dist/testing/runner/lifecycle.js.map +1 -0
  148. package/dist/testing/runner/run-from-metadata.d.ts +24 -0
  149. package/dist/testing/runner/run-from-metadata.d.ts.map +1 -0
  150. package/dist/testing/runner/run-from-metadata.js +70 -0
  151. package/dist/testing/runner/run-from-metadata.js.map +1 -0
  152. package/dist/testing/runner/scenario-filter.d.ts +9 -0
  153. package/dist/testing/runner/scenario-filter.d.ts.map +1 -0
  154. package/dist/testing/runner/scenario-filter.js +22 -0
  155. package/dist/testing/runner/scenario-filter.js.map +1 -0
  156. package/dist/testing/steps/api/auth.step.d.ts +3 -0
  157. package/dist/testing/steps/api/auth.step.d.ts.map +1 -0
  158. package/dist/testing/steps/api/auth.step.js +38 -0
  159. package/dist/testing/steps/api/auth.step.js.map +1 -0
  160. package/dist/testing/steps/api/index.d.ts +3 -0
  161. package/dist/testing/steps/api/index.d.ts.map +1 -0
  162. package/dist/testing/steps/api/index.js +10 -0
  163. package/dist/testing/steps/api/index.js.map +1 -0
  164. package/dist/testing/steps/api/request.step.d.ts +3 -0
  165. package/dist/testing/steps/api/request.step.d.ts.map +1 -0
  166. package/dist/testing/steps/api/request.step.js +281 -0
  167. package/dist/testing/steps/api/request.step.js.map +1 -0
  168. package/dist/testing/steps/assert/http.step.d.ts +3 -0
  169. package/dist/testing/steps/assert/http.step.d.ts.map +1 -0
  170. package/dist/testing/steps/assert/http.step.js +27 -0
  171. package/dist/testing/steps/assert/http.step.js.map +1 -0
  172. package/dist/testing/steps/assert/index.d.ts +3 -0
  173. package/dist/testing/steps/assert/index.d.ts.map +1 -0
  174. package/dist/testing/steps/assert/index.js +12 -0
  175. package/dist/testing/steps/assert/index.js.map +1 -0
  176. package/dist/testing/steps/assert/jsonpath.step.d.ts +3 -0
  177. package/dist/testing/steps/assert/jsonpath.step.d.ts.map +1 -0
  178. package/dist/testing/steps/assert/jsonpath.step.js +40 -0
  179. package/dist/testing/steps/assert/jsonpath.step.js.map +1 -0
  180. package/dist/testing/steps/assert/primitives.step.d.ts +3 -0
  181. package/dist/testing/steps/assert/primitives.step.d.ts.map +1 -0
  182. package/dist/testing/steps/assert/primitives.step.js +43 -0
  183. package/dist/testing/steps/assert/primitives.step.js.map +1 -0
  184. package/dist/testing/steps/test/index.d.ts +3 -0
  185. package/dist/testing/steps/test/index.d.ts.map +1 -0
  186. package/dist/testing/steps/test/index.js +8 -0
  187. package/dist/testing/steps/test/index.js.map +1 -0
  188. package/dist/testing/steps/test/test-spec.step.d.ts +3 -0
  189. package/dist/testing/steps/test/test-spec.step.d.ts.map +1 -0
  190. package/dist/testing/steps/test/test-spec.step.js +41 -0
  191. package/dist/testing/steps/test/test-spec.step.js.map +1 -0
  192. package/dist/testing/steps/ui/actions.step.d.ts +3 -0
  193. package/dist/testing/steps/ui/actions.step.d.ts.map +1 -0
  194. package/dist/testing/steps/ui/actions.step.js +31 -0
  195. package/dist/testing/steps/ui/actions.step.js.map +1 -0
  196. package/dist/testing/steps/ui/assertions.step.d.ts +3 -0
  197. package/dist/testing/steps/ui/assertions.step.d.ts.map +1 -0
  198. package/dist/testing/steps/ui/assertions.step.js +41 -0
  199. package/dist/testing/steps/ui/assertions.step.js.map +1 -0
  200. package/dist/testing/steps/ui/form.step.d.ts +3 -0
  201. package/dist/testing/steps/ui/form.step.d.ts.map +1 -0
  202. package/dist/testing/steps/ui/form.step.js +34 -0
  203. package/dist/testing/steps/ui/form.step.js.map +1 -0
  204. package/dist/testing/steps/ui/index.d.ts +3 -0
  205. package/dist/testing/steps/ui/index.d.ts.map +1 -0
  206. package/dist/testing/steps/ui/index.js +14 -0
  207. package/dist/testing/steps/ui/index.js.map +1 -0
  208. package/dist/testing/steps/ui/navigation.step.d.ts +3 -0
  209. package/dist/testing/steps/ui/navigation.step.d.ts.map +1 -0
  210. package/dist/testing/steps/ui/navigation.step.js +39 -0
  211. package/dist/testing/steps/ui/navigation.step.js.map +1 -0
  212. package/dist/testing/steps/util/index.d.ts +3 -0
  213. package/dist/testing/steps/util/index.d.ts.map +1 -0
  214. package/dist/testing/steps/util/index.js +12 -0
  215. package/dist/testing/steps/util/index.js.map +1 -0
  216. package/dist/testing/steps/util/log.step.d.ts +3 -0
  217. package/dist/testing/steps/util/log.step.d.ts.map +1 -0
  218. package/dist/testing/steps/util/log.step.js +18 -0
  219. package/dist/testing/steps/util/log.step.js.map +1 -0
  220. package/dist/testing/steps/util/require.step.d.ts +3 -0
  221. package/dist/testing/steps/util/require.step.d.ts.map +1 -0
  222. package/dist/testing/steps/util/require.step.js +16 -0
  223. package/dist/testing/steps/util/require.step.js.map +1 -0
  224. package/dist/testing/steps/util/sleep.step.d.ts +3 -0
  225. package/dist/testing/steps/util/sleep.step.d.ts.map +1 -0
  226. package/dist/testing/steps/util/sleep.step.js +13 -0
  227. package/dist/testing/steps/util/sleep.step.js.map +1 -0
  228. package/docs/test-data-workflow.md +51 -11
  229. package/package.json +4 -2
  230. package/src/commands/run-tests.command.ts +278 -0
  231. package/src/commands/test-data.command.ts +26 -26
  232. package/src/commands/test.command.ts +14 -0
  233. package/src/controllers/service.controller.ts +58 -59
  234. package/src/dtos/basic-filters.dto.ts +0 -2
  235. package/src/dtos/create-user.dto.ts +1 -0
  236. package/src/helpers/schematic.service.ts +1 -1
  237. package/src/index.ts +3 -0
  238. package/src/seeders/module-metadata-seeder.service.ts +5 -25
  239. package/src/seeders/module-test-data.service.ts +5 -3
  240. package/src/seeders/seed-data/solid-core-metadata.json +34 -9
  241. package/src/services/chatter-message.service.ts +18 -1
  242. package/src/services/crud.service.ts +1 -0
  243. package/src/services/model-metadata.service.ts +2 -1
  244. package/src/services/module-metadata.service.ts +2 -1
  245. package/src/services/queues/common.ts +75 -0
  246. package/src/services/queues/database-publisher.service.ts +4 -1
  247. package/src/services/queues/database-subscriber.service.ts +5 -3
  248. package/src/services/queues/rabbitmq-publisher.service.ts +17 -7
  249. package/src/services/queues/rabbitmq-subscriber.service.ts +223 -95
  250. package/src/solid-core.module.ts +4 -0
  251. package/src/testing/README.md +364 -0
  252. package/src/testing/__examples__/register-example-specs.ts +6 -0
  253. package/src/testing/__examples__/specs/custom-health.spec.ts +29 -0
  254. package/src/testing/__examples__/testing.sample.json +82 -0
  255. package/src/testing/adapters/api/api-adapter.ts +85 -0
  256. package/src/testing/adapters/api/api.types.ts +15 -0
  257. package/src/testing/adapters/ui/playwright-adapter.ts +54 -0
  258. package/src/testing/adapters/ui/ui.types.ts +4 -0
  259. package/src/testing/contracts/runtime-context.types.ts +36 -0
  260. package/src/testing/contracts/test-spec.types.ts +24 -0
  261. package/src/testing/contracts/testing-metadata.types.ts +46 -0
  262. package/src/testing/core/interpolation.ts +189 -0
  263. package/src/testing/core/normalize-steps.ts +21 -0
  264. package/src/testing/core/resource-store.ts +38 -0
  265. package/src/testing/core/spec-registry.ts +33 -0
  266. package/src/testing/core/step-registry.ts +27 -0
  267. package/src/testing/core/testing-engine.ts +127 -0
  268. package/src/testing/core/timeout.ts +19 -0
  269. package/src/testing/reporter/attachments.ts +25 -0
  270. package/src/testing/reporter/console-reporter.ts +229 -0
  271. package/src/testing/reporter/reporter.types.ts +36 -0
  272. package/src/testing/runner/lifecycle.ts +31 -0
  273. package/src/testing/runner/run-from-metadata.ts +87 -0
  274. package/src/testing/runner/scenario-filter.ts +33 -0
  275. package/src/testing/steps/api/auth.step.ts +66 -0
  276. package/src/testing/steps/api/index.ts +10 -0
  277. package/src/testing/steps/api/request.step.ts +358 -0
  278. package/src/testing/steps/assert/http.step.ts +33 -0
  279. package/src/testing/steps/assert/index.ts +12 -0
  280. package/src/testing/steps/assert/jsonpath.step.ts +50 -0
  281. package/src/testing/steps/assert/primitives.step.ts +69 -0
  282. package/src/testing/steps/test/index.ts +8 -0
  283. package/src/testing/steps/test/test-spec.step.ts +52 -0
  284. package/src/testing/steps/ui/actions.step.ts +36 -0
  285. package/src/testing/steps/ui/assertions.step.ts +54 -0
  286. package/src/testing/steps/ui/form.step.ts +39 -0
  287. package/src/testing/steps/ui/index.ts +12 -0
  288. package/src/testing/steps/ui/navigation.step.ts +53 -0
  289. package/src/testing/steps/util/index.ts +10 -0
  290. package/src/testing/steps/util/log.step.ts +19 -0
  291. package/src/testing/steps/util/require.step.ts +16 -0
  292. package/src/testing/steps/util/sleep.step.ts +15 -0
  293. package/tsconfig.json +35 -25
  294. package/tsconfig.tests.json +14 -0
  295. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -5,6 +5,7 @@ import { QueuesModuleOptions } from "../../interfaces";
5
5
  import { QueueMessage, QueuePublisher } from '../../interfaces/mq';
6
6
  import { MqMessageQueueService } from '../mq-message-queue.service';
7
7
  import { MqMessageService } from '../mq-message.service';
8
+ import { buildNamespacedQueueName } from './common';
8
9
 
9
10
  export abstract class RabbitMqPublisher<T> implements OnModuleDestroy, QueuePublisher<T> {
10
11
  private readonly logger = new Logger(RabbitMqPublisher.name);
@@ -70,13 +71,21 @@ export abstract class RabbitMqPublisher<T> implements OnModuleDestroy, QueuePubl
70
71
 
71
72
  const channel = await conn.createChannel();
72
73
 
74
+ channel.on('return', (msg) => {
75
+ const content = msg.content?.toString?.() ?? '';
76
+ this.logger.warn(
77
+ `RabbitMqPublisher message returned from exchange ${msg.fields.exchange} with routingKey ${msg.fields.routingKey}: ${content}`,
78
+ );
79
+ });
80
+
73
81
  const options = this.options();
74
82
  const queueName = options.queueName;
75
- const exchangeName = `${queueName}.exchange`;
76
- const routingKey = `${queueName}.routing-key`;
83
+ const namespacedQueueName = buildNamespacedQueueName(queueName);
84
+ const exchangeName = `${namespacedQueueName}.exchange`;
85
+ const routingKey = `${namespacedQueueName}.routing-key`;
77
86
 
78
87
  await channel.assertExchange(exchangeName, 'direct', {});
79
- const queue = await channel.assertQueue(queueName, {});
88
+ const queue = await channel.assertQueue(namespacedQueueName, {});
80
89
  await channel.bindQueue(queue.queue, exchangeName, routingKey);
81
90
 
82
91
  this.connection = conn;
@@ -127,7 +136,6 @@ export abstract class RabbitMqPublisher<T> implements OnModuleDestroy, QueuePubl
127
136
  }
128
137
  }
129
138
 
130
-
131
139
  async publish(message: QueueMessage<T>): Promise<string> {
132
140
  if (!this.url) {
133
141
  this.logger.error('RabbitMqPublisher url is not defined in the environment variables');
@@ -148,8 +156,10 @@ export abstract class RabbitMqPublisher<T> implements OnModuleDestroy, QueuePubl
148
156
  const options = this.options();
149
157
 
150
158
  const queueName = options.queueName;
151
- const exchangeName = `${queueName}.exchange`;
152
- const routingKey = `${queueName}.routing-key`;
159
+ const namespacedQueueName = buildNamespacedQueueName(queueName);
160
+
161
+ const exchangeName = `${namespacedQueueName}.exchange`;
162
+ const routingKey = `${namespacedQueueName}.routing-key`;
153
163
 
154
164
  // Set default values for retry.
155
165
  // by default there are no retries.
@@ -160,7 +170,7 @@ export abstract class RabbitMqPublisher<T> implements OnModuleDestroy, QueuePubl
160
170
  message.messageId = uuidv4();
161
171
 
162
172
  // Save the message to the DB so that we can then change its status in the subscriber...
163
- await this.persistToDatabase(queueName, message);
173
+ await this.persistToDatabase(namespacedQueueName, message);
164
174
 
165
175
  // wait for the channel to confirm
166
176
  try {
@@ -4,17 +4,21 @@ import { QueuesModuleOptions } from "../../interfaces";
4
4
  import { QueueMessage, QueueSubscriber } from '../../interfaces/mq';
5
5
  import { MqMessageQueueService } from '../mq-message-queue.service';
6
6
  import { MqMessageService } from '../mq-message.service';
7
+ import { buildNamespacedQueueName } from './common';
7
8
 
8
9
 
9
10
  export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscriber<T> { // TODO This can be made a generic type for better type visibility
10
11
  private readonly logger = new Logger(RabbitMqSubscriber.name);
11
12
  private readonly url: string;
12
13
  private readonly serviceRole: string;
13
-
14
- constructor(
15
- protected readonly mqMessageService: MqMessageService,
16
- protected readonly mqMessageQueueService: MqMessageQueueService,
17
- ) {
14
+ private connection: amqp.Connection | null = null;
15
+ private channel: amqp.Channel | null = null;
16
+ private consumerTag: string | null = null;
17
+ private reconnectPromise: Promise<void> | null = null;
18
+ private reconnectAttempt = 0;
19
+ private stopping = false;
20
+
21
+ constructor(protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService) {
18
22
  this.url = process.env.QUEUES_RABBIT_MQ_URL;
19
23
  this.serviceRole = process.env.QUEUES_SERVICE_ROLE;
20
24
  if (!this.url) {
@@ -46,6 +50,7 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
46
50
  username: url.username,
47
51
  password: decodeURIComponent(url.password),
48
52
  frameMax: 131072,
53
+ heartbeat: 30,
49
54
  });
50
55
 
51
56
  return connection
@@ -76,86 +81,238 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
76
81
  }
77
82
  }
78
83
 
79
- // this.logger.debug(`RabbitMqSubscriber instance created with options: ${JSON.stringify(this.options())} and url: ${this.url}`);
80
- // const connection = await amqp.connect(this.url);
81
-
82
- let connection;
84
+ const namespacedQueueName = buildNamespacedQueueName(queueName);
83
85
  try {
84
- connection = await this.establishConnection();
85
- // this.logger.debug(`RabbitMqSubscriber connection established: ${JSON.stringify(this.options())} and url: ${this.url}`);
86
+ await this.connectAndConsume(namespacedQueueName);
87
+ } catch (err) {
88
+ this.logger.error(`Failed to connect to RabbitMQ for queue ${namespacedQueueName}: ${(err as Error).message}`, (err as Error).stack);
89
+ this.triggerReconnect(namespacedQueueName, 'initial connection failure');
86
90
  }
87
- catch (err) {
88
- this.logger.error(`Failed to connect to RabbitMQ: ${(err as Error).message}`, (err as Error).stack);
89
- throw err;
91
+
92
+ this.logger.log(`RabbitMqSubscriber ready to consume messages: ${JSON.stringify(options)} and url: ${this.url}`);
93
+ }
94
+ }
95
+
96
+ private async connectAndConsume(queueName: string): Promise<void> {
97
+ await this.cleanup();
98
+ this.logger.log(`RabbitMqSubscriber in connectAndConsume for queue: ${queueName} and url: ${this.url}`);
99
+
100
+ let connection: amqp.Connection;
101
+ try {
102
+ connection = await this.establishConnection();
103
+ } catch (err) {
104
+ this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack);
105
+ throw err;
106
+ }
107
+
108
+ this.connection = connection;
109
+
110
+ connection.on('error', (err) => {
111
+ if (connection !== this.connection) return;
112
+ this.logger.error(`RabbitMqSubscriber connection error for queue ${queueName}: ${(err as Error).message}`);
113
+ });
114
+
115
+ connection.on('close', () => {
116
+ if (connection !== this.connection) return;
117
+ this.logger.warn(`RabbitMqSubscriber connection closed for queue ${queueName}`);
118
+ this.triggerReconnect(queueName, 'connection closed');
119
+ });
120
+
121
+ const channel = await connection.createChannel();
122
+ this.channel = channel;
123
+
124
+ channel.on('error', (err) => {
125
+ if (channel !== this.channel) return;
126
+ this.logger.error(`RabbitMqSubscriber channel error for queue ${queueName}: ${(err as Error).message}`);
127
+ });
128
+
129
+ channel.on('close', () => {
130
+ if (channel !== this.channel) return;
131
+ this.logger.warn(`RabbitMqSubscriber channel closed for queue ${queueName}`);
132
+ this.triggerReconnect(queueName, 'channel closed');
133
+ });
134
+
135
+ // Process one message at a time per consumer to avoid parallel work on the same subscriber instance.
136
+ await channel.prefetch(1);
137
+
138
+ // Use a direct exchange with a stable routing key so retry DLX can route back to the main queue.
139
+ const exchangeName = `${queueName}.exchange`;
140
+ const routingKey = `${queueName}.routing-key`;
141
+ const retryQueue = `${queueName}.retry`;
142
+ const failedQueue = `${queueName}.failed`;
143
+
144
+ await channel.assertExchange(exchangeName, 'direct', {});
145
+ await channel.assertQueue(queueName, {});
146
+ await channel.bindQueue(queueName, exchangeName, routingKey);
147
+
148
+ // Retry queue uses DLX to route expired messages back to the main exchange/routing key.
149
+ await channel.assertQueue(retryQueue, {
150
+ arguments: {
151
+ 'x-dead-letter-exchange': exchangeName,
152
+ 'x-dead-letter-routing-key': routingKey,
90
153
  }
154
+ });
91
155
 
92
- const channel = await connection.createChannel();
93
- // this.logger.debug(`RabbitMqSubscriber channel created: ${JSON.stringify(this.options())} and url: ${url}`);
156
+ await channel.assertQueue(failedQueue, {});
157
+
158
+ const consumeResult = await channel.consume(
159
+ queueName,
160
+ async (rawMessage) => {
161
+ if (!rawMessage) {
162
+ return;
163
+ }
94
164
 
95
- const exchangeName = `${queueName}.exchange`;
96
- const routingKey = `${queueName}.routing-key`;
165
+ let message: QueueMessage<T> = null;
97
166
 
98
- await channel.assertExchange(exchangeName, 'direct', {});
99
- // this.logger.debug(`RabbitMqSubscriber channel asserted: ${JSON.stringify(this.options())} and url: ${url}`);
167
+ try {
168
+ const messageContentString = rawMessage.content.toString();
169
+ message = JSON.parse(messageContentString) as QueueMessage<T>;
170
+ this.logger.debug(`rabbitmq subscriber received message with id: ${message.messageId} for queue ${queueName}`);
171
+ } catch (error) {
172
+ this.logger.error(`Invalid JSON message on queue ${queueName}: ${(error as Error).message}`);
173
+ await this.publishToFailedQueue(queueName, rawMessage.content, channel, error);
174
+ channel.ack(rawMessage);
175
+ return;
176
+ }
100
177
 
101
- const queue = await channel.assertQueue(queueName, {});
102
- // this.logger.debug(`RabbitMqSubscriber queue asserted: ${JSON.stringify(this.options())} and url: ${url}`);
178
+ if (!message.retryCount) message.retryCount = 0;
179
+ if (!message.retryInterval) message.retryInterval = 1000;
180
+ if (!message.currentRetry) message.currentRetry = 0;
103
181
 
104
- await channel.bindQueue(queue.queue, exchangeName, routingKey);
105
- // this.logger.debug(`RabbitMqSubscriber queue bound: ${JSON.stringify(this.options())} and url: ${url}`);
182
+ try {
183
+ await this.processMessage(message, rawMessage, channel);
184
+ } catch (error) {
185
+ await this.handleProcessingError(message, rawMessage, channel, error, queueName);
186
+ }
187
+ },
188
+ // Explicit ack enables reliable processing and retry routing.
189
+ { noAck: false },
190
+ );
106
191
 
107
- // Consume messages from the queue
108
- channel.consume(
109
- queue.queue,
110
- async (rawMessage) => {
111
- if (rawMessage) {
112
- const messageContentString = rawMessage.content.toString();
113
- // this.logger.debug(`RabbitMqSubscriber Received raw message: ${messageContentString}`);
192
+ this.consumerTag = consumeResult.consumerTag;
193
+ }
114
194
 
115
- let message: QueueMessage<T> = null;
195
+ // Retry flow: update DB -> increment retry -> send to retry queue with per-message expiration -> ack original.
196
+ private async handleProcessingError(message: QueueMessage<T>, rawMessage: amqp.ConsumeMessage, channel: amqp.Channel, error: any, queueName: string): Promise<void> {
197
+ const errorMessage = (error as Error)?.message || String(error);
198
+ this.logger.error(`Error processing message on queue ${queueName}: ${errorMessage}`);
116
199
 
117
- try {
118
- message = JSON.parse(messageContentString) as QueueMessage<T>;
200
+ if (message.currentRetry < message.retryCount) {
201
+ await this.updateStatusInDatabase('retrying', message);
119
202
 
120
- // this is the first time we are receiving the message so we set the currentRetry to 0
121
- if (!message.retryCount) message.retryCount = 0;
122
- if (!message.retryInterval) message.retryInterval = 1000;
123
- if (!message.currentRetry) message.currentRetry = 0;
203
+ message.currentRetry++;
204
+ const retryQueue = `${queueName}.retry`;
205
+ const payload = Buffer.from(JSON.stringify(message));
124
206
 
125
- await this.processMessage(message, rawMessage, channel);
126
- }
127
- catch (error) {
128
- this.logger.error(`Error processing message: ${error.message}`);
207
+ // Per-message expiration keeps the message in the retry queue until TTL, then DLX routes it back.
208
+ channel.sendToQueue(retryQueue, payload, {
209
+ expiration: String(message.retryInterval || 1000),
210
+ headers: {
211
+ 'x-error': errorMessage,
212
+ }
213
+ });
129
214
 
130
- // if an error occurs then if retryCount is set we start retrying.
131
- if (message) {
132
- if (message.currentRetry < message.retryCount) {
133
- await this.updateStatusInDatabase('retrying', message);
215
+ channel.ack(rawMessage);
216
+ this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms on queue ${queueName}`);
217
+ return;
218
+ }
134
219
 
135
- message.currentRetry++;
136
- this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms`);
137
- setTimeout(() => {
138
- this.retryMessage(message, rawMessage, channel);
139
- }, message.retryInterval);
140
- } else {
141
- await this.updateStatusInDatabase('failed', message, error.message, '');
220
+ await this.updateStatusInDatabase('failed', message, errorMessage, '');
221
+ channel.ack(rawMessage);
222
+ await this.publishToFailedQueue(queueName, Buffer.from(JSON.stringify(message)), channel, error);
223
+ this.logger.error(`Message failed after ${message.retryCount} attempts on queue ${queueName}: ${errorMessage}`);
224
+ }
142
225
 
143
- this.logger.error(`Message failed after ${message.retryCount} attempts: ${error.message}`);
144
- channel.ack(rawMessage); // Discard the message after max retries
145
- }
146
- }
226
+ private async publishToFailedQueue(queueName: string, payload: Buffer | string, channel: amqp.Channel, error?: any): Promise<void> {
227
+ const failedQueue = `${queueName}.failed`;
228
+ const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
229
+ const errorMessage = (error as Error)?.message || String(error || '');
147
230
 
148
- }
149
- }
150
- },
151
- // { noAck: true },
152
- {},
153
- );
231
+ try {
232
+ channel.sendToQueue(failedQueue, body, errorMessage ? {
233
+ headers: { 'x-error': errorMessage }
234
+ } : undefined);
235
+ } catch (err) {
236
+ this.logger.error(`Failed to publish to failed queue ${failedQueue}: ${(err as Error).message}`);
237
+ }
238
+ }
154
239
 
155
- this.logger.log(`RabbitMqSubscriber ready to consume messages: ${JSON.stringify(this.options())} and url: ${this.url}`);
240
+ private triggerReconnect(queueName: string, reason: string) {
241
+ if (this.stopping) return;
242
+ if (this.reconnectPromise) return;
243
+
244
+ this.reconnectPromise = this.reconnectLoop(queueName, reason)
245
+ .finally(() => {
246
+ this.reconnectPromise = null;
247
+ });
248
+ }
249
+
250
+ // Reconnect with backoff to avoid hammering the broker during outages.
251
+ private async reconnectLoop(queueName: string, reason: string): Promise<void> {
252
+ this.logger.warn(`RabbitMqSubscriber reconnecting for queue ${queueName}: ${reason}`);
253
+
254
+ while (!this.stopping) {
255
+ try {
256
+ await this.connectAndConsume(queueName);
257
+ this.reconnectAttempt = 0;
258
+ this.logger.log(`RabbitMqSubscriber reconnected for queue ${queueName}`);
259
+ return;
260
+ } catch (err) {
261
+ this.reconnectAttempt += 1;
262
+ const delay = this.backoff();
263
+ this.logger.warn(`RabbitMqSubscriber reconnect failed for queue ${queueName}; retrying in ${delay}ms`);
264
+ await this.sleep(delay);
265
+ }
156
266
  }
157
267
  }
158
268
 
269
+ private async cleanup(): Promise<void> {
270
+ const channel = this.channel;
271
+ const connection = this.connection;
272
+ const consumerTag = this.consumerTag;
273
+
274
+ this.channel = null;
275
+ this.connection = null;
276
+ this.consumerTag = null;
277
+
278
+ if (channel) {
279
+ try {
280
+ if (consumerTag) {
281
+ await channel.cancel(consumerTag);
282
+ }
283
+ } catch (_) {
284
+ // ignore
285
+ }
286
+
287
+ try {
288
+ await channel.close();
289
+ } catch (_) {
290
+ // ignore
291
+ }
292
+ }
293
+
294
+ if (connection) {
295
+ try {
296
+ await connection.close();
297
+ } catch (_) {
298
+ // ignore
299
+ }
300
+ }
301
+ }
302
+
303
+ private sleep(ms: number): Promise<void> {
304
+ return new Promise(resolve => setTimeout(resolve, ms));
305
+ }
306
+
307
+ // Exponential backoff with jitter, capped to 30s.
308
+ private backoff(): number {
309
+ const baseMs = 1000;
310
+ const maxMs = 30_000;
311
+ const exp = Math.min(maxMs, baseMs * Math.pow(2, this.reconnectAttempt));
312
+ const jitter = Math.floor(Math.random() * (exp * 0.2));
313
+ return Math.min(maxMs, exp + jitter);
314
+ }
315
+
159
316
  /**
160
317
  * Abstract method for message processing logic.
161
318
  */
@@ -168,43 +325,14 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
168
325
  // Ack the message.
169
326
  channel.ack(rawMessage);
170
327
 
171
- // TODO: Update the database to indicate that the task is finished.
328
+ // Persist success output and timing.
172
329
  await this.updateStatusInDatabase('succeeded', message, '', result ? JSON.stringify(result, null, 2) : '');
173
330
 
174
331
  }
175
332
 
176
- /**
177
- * Retry the message by invoking the processing logic again.
178
- */
179
- private async retryMessage(message: QueueMessage<T>, rawMessage, channel) {
180
- try {
181
- await this.processMessage(message, rawMessage, channel);
182
- } catch (error) {
183
- if (message.currentRetry < message.retryCount) {
184
- await this.updateStatusInDatabase('retrying', message);
185
-
186
- message.currentRetry++;
187
- this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms: ${error.message}`);
188
- setTimeout(() => {
189
- this.retryMessage(message, rawMessage, channel);
190
- }, message.retryInterval);
191
- } else {
192
-
193
- this.logger.error(`Message failed after ${message.retryCount} attempts: ${error.message}`);
194
-
195
- // Discard the message after max retries
196
- channel.ack(rawMessage);
197
-
198
- // TODO: Store the error in the database and update the status accordingly.
199
- await this.updateStatusInDatabase('failed', message, error.message, '');
200
-
201
- }
202
- }
203
- }
204
-
205
333
  private async updateStatusInDatabase(stage: string, message: QueueMessage<T>, error: string = '', result: string = '') {
206
334
 
207
- // TODO: make an entry in the relevant database table, generate a unique id earlier.
335
+ // Update the existing message record by messageId; creation happens upstream.
208
336
  try {
209
337
  // 1. resolve the queue first
210
338
  const mqMessage = await this.mqMessageService.repo.findOne({
@@ -58,6 +58,8 @@ import { HttpModule } from '@nestjs/axios';
58
58
  import { JwtModule } from '@nestjs/jwt';
59
59
  import { SeedCommand } from './commands/seed.command';
60
60
  import { TestDataCommand } from './commands/test-data.command';
61
+ import { TestRunCommand } from './commands/run-tests.command';
62
+ import { TestCommand } from './commands/test.command';
61
63
  import { AuthenticationController } from './controllers/authentication.controller';
62
64
  import { EmailTemplateController } from './controllers/email-template.controller';
63
65
  import { GoogleAuthenticationController } from './controllers/google-authentication.controller';
@@ -506,7 +508,9 @@ import { ListOfRolesSelectionProvider } from './services/selection-providers/lis
506
508
  TextractService,
507
509
  SolidRegistry,
508
510
  SeedCommand,
511
+ TestCommand,
509
512
  TestDataCommand,
513
+ TestRunCommand,
510
514
  McpCommand,
511
515
  IngestCommand,
512
516
  IngestMetadataService,