@mojaloop/central-ledger 19.14.0 → 20.1.0

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 (394) hide show
  1. package/.grype.yaml +3 -0
  2. package/.license-scanner.json +16 -0
  3. package/.ncurc.yaml +19 -13
  4. package/.nvmrc +1 -1
  5. package/.nycrc.yml +14 -2
  6. package/CHANGELOG.md +51 -0
  7. package/Dockerfile +13 -10
  8. package/README.md +39 -89
  9. package/audit-ci.jsonc +1 -1
  10. package/config/default.json +60 -3
  11. package/config/deprecated.default-settlement.json +160 -0
  12. package/docker/ml-api-adapter/default.json +28 -0
  13. package/docker-compose.yml +6 -0
  14. package/nodemon.json +6 -0
  15. package/package.json +128 -64
  16. package/{sbom-v19.13.2.csv → sbom-v20.0.0.csv} +886 -736
  17. package/scripts/_wait4_all.js +5 -5
  18. package/scripts/transferSettlementTemp/interchangeFeeCalculation.js +51 -0
  19. package/src/api/index.ts +30 -0
  20. package/src/api/interface/swagger.json +2249 -2054
  21. package/src/api/participants/routes.js +1 -1
  22. package/src/api/root/handler.int.ts +55 -0
  23. package/src/api/routes.js +7 -6
  24. package/src/api/server.ts +27 -0
  25. package/src/api/server.unit.ts +16 -0
  26. package/src/api/settlementModels/routes.js +4 -4
  27. package/src/domain/participant/index.d.ts +47 -0
  28. package/src/domain/participant/index.int.ts +724 -0
  29. package/src/domain/participant/index.js +4 -5
  30. package/src/domain/position/fx-prepare.js +1 -1
  31. package/src/domain/position/fx-timeout-reserved.js +6 -0
  32. package/src/domain/position/prepare.js +1 -1
  33. package/src/handlers/admin/handler.js +1 -1
  34. package/src/handlers/index.js +30 -0
  35. package/src/handlers/positions/handler.js +0 -2
  36. package/src/handlers/positions/handlerBatch.js +5 -5
  37. package/src/handlers/register.js +44 -29
  38. package/src/handlers/timeouts/handler.js +1 -4
  39. package/src/handlers/transfers/fx-abort.int.ts +209 -0
  40. package/src/handlers/transfers/fx-handlers.int.ts +275 -0
  41. package/src/handlers/transfers/fx-timeout.int.ts +284 -0
  42. package/src/handlers/transfers/handler.js +1 -3
  43. package/src/handlers/transfers/handlers.int.ts +538 -0
  44. package/src/handlers/transfers/prepare.int.ts +1283 -0
  45. package/src/handlers/transfers/prepare.js +2 -2
  46. package/src/knexfile.js +19 -0
  47. package/src/lib/cache.js +1 -1
  48. package/src/lib/config/index.ts +60 -0
  49. package/src/lib/config/resolver.ts +231 -0
  50. package/src/lib/config/resolver.unit.ts +72 -0
  51. package/src/lib/config/types.ts +372 -0
  52. package/src/lib/config/util.ts +523 -0
  53. package/src/lib/config/util.unit.ts +825 -0
  54. package/src/lib/migrator.js +3 -3
  55. package/src/lib/proxyCache.js +3 -3
  56. package/{migrations → src/migrations}/100100_event.js +1 -1
  57. package/{migrations → src/migrations}/110100_contactType.js +1 -1
  58. package/{migrations → src/migrations}/110200_currency.js +1 -1
  59. package/{migrations → src/migrations}/110201_currency-scale.js +1 -1
  60. package/{migrations → src/migrations}/110300_endpointType.js +1 -1
  61. package/{migrations → src/migrations}/110400_ledgerEntryType.js +1 -1
  62. package/{migrations → src/migrations}/110450_ledgerAccountType.js +1 -1
  63. package/{migrations → src/migrations}/110500_participantLimitType.js +1 -1
  64. package/{migrations → src/migrations}/110600_transferParticipantRoleType.js +1 -1
  65. package/{migrations → src/migrations}/110700_transferState.js +1 -1
  66. package/{migrations → src/migrations}/110900_settlementState.js +1 -1
  67. package/{migrations → src/migrations}/112000_bulkTransferState.js +1 -1
  68. package/{migrations → src/migrations}/112100_bulkProcessingState.js +1 -1
  69. package/{migrations → src/migrations}/200100_participant.js +1 -1
  70. package/{migrations → src/migrations}/200200_participantContact.js +1 -1
  71. package/{migrations → src/migrations}/200300_participantEndpoint.js +1 -1
  72. package/{migrations → src/migrations}/200400_participantParty.js +1 -1
  73. package/{migrations → src/migrations}/200600_token.js +1 -1
  74. package/{migrations → src/migrations}/200601_token-indexes.js +1 -1
  75. package/{migrations → src/migrations}/300100_transferDuplicateCheck.js +2 -2
  76. package/{migrations → src/migrations}/300150_bulkTransferDuplicateCheck.js +2 -2
  77. package/{migrations → src/migrations}/300200_transfer.js +2 -2
  78. package/{migrations → src/migrations}/300202_transfer-decimal184.js +1 -1
  79. package/{migrations → src/migrations}/300250_bulkTransfer.js +2 -3
  80. package/{migrations → src/migrations}/300275_bulkTransferAssociation.js +2 -3
  81. package/{migrations → src/migrations}/300300_ilpPacket.js +1 -1
  82. package/{migrations → src/migrations}/300400_transferStateChange.js +1 -1
  83. package/{migrations → src/migrations}/300450_bulkTransferStateChange.js +1 -1
  84. package/{migrations → src/migrations}/300500_segment.js +1 -1
  85. package/{migrations → src/migrations}/310100_participantCurrency.js +1 -1
  86. package/{migrations → src/migrations}/310200_transferParticipant.js +1 -1
  87. package/{migrations → src/migrations}/310202_transferParticipant-decimal184.js +1 -1
  88. package/{migrations → src/migrations}/310204_transferParticipant-participantId.js +2 -2
  89. package/{migrations → src/migrations}/310300_participantPosition.js +1 -1
  90. package/{migrations → src/migrations}/310302_participantPosition-decimal184.js +1 -1
  91. package/{migrations → src/migrations}/310400_participantPositionChange.js +1 -1
  92. package/{migrations → src/migrations}/310402_participantPositionChange-decimal184.js +1 -1
  93. package/{migrations → src/migrations}/310403_participantPositionChange-participantCurrencyId.js +2 -2
  94. package/{migrations → src/migrations}/310404_participantPositionChange-change.js +2 -2
  95. package/{migrations → src/migrations}/310500_participantLimit.js +1 -1
  96. package/{migrations → src/migrations}/310502_participantLimit-decimal184.js +1 -1
  97. package/{migrations → src/migrations}/400100_settlementWindow.js +1 -1
  98. package/{migrations → src/migrations}/400200_settlement.js +1 -1
  99. package/{migrations → src/migrations}/400300_settlementSettlementWindow.js +1 -1
  100. package/{migrations → src/migrations}/400301_settlementSettlementWindow-indexes.js +0 -1
  101. package/{migrations → src/migrations}/400400_settlementWindowStateChange.js +1 -1
  102. package/{migrations → src/migrations}/400500_settlementTransferParticipant.js +1 -1
  103. package/{migrations → src/migrations}/400502_settlementTransferParticipant-decimal184.js +1 -1
  104. package/{migrations → src/migrations}/400600_settlementParticipantCurrency.js +1 -1
  105. package/{migrations → src/migrations}/400602_settlementParticipantCurrency-decimal184.js +1 -1
  106. package/{migrations → src/migrations}/400700_settlementParticipantCurrencyStateChange.js +1 -1
  107. package/{migrations → src/migrations}/400800_settlementStateChange.js +1 -1
  108. package/{migrations → src/migrations}/400900_settlementWindowContent.js +1 -1
  109. package/{migrations → src/migrations}/401000_settlementWindowContentStateChange.js +1 -1
  110. package/{migrations → src/migrations}/401001_settlementWindowContentStateChange-indexes.js +6 -6
  111. package/{migrations → src/migrations}/401200_settlementContentAggregation.js +1 -1
  112. package/{migrations → src/migrations}/401300_settlementModel.js +1 -1
  113. package/{migrations → src/migrations}/410100_transferFulfilmentDuplicateCheck.js +1 -1
  114. package/{migrations → src/migrations}/410102_transferFulfilmentDuplicateCheck-dropForeignTransferId.js +1 -1
  115. package/{migrations → src/migrations}/410103_transferFulfilmentDuplicateCheck-hashNullable.js +1 -1
  116. package/{migrations → src/migrations}/410200_transferFulfilment.js +1 -1
  117. package/{migrations → src/migrations}/410202_transferFulfilment-ilpFulfilment.js +1 -1
  118. package/{migrations → src/migrations}/410203_transferFulfilment-foreignTransferFulfilmentId.js +1 -1
  119. package/{migrations → src/migrations}/410240_bulkTransferFulfilmentDuplicateCheck.js +1 -1
  120. package/{migrations → src/migrations}/410250_bulkTransferFulfilment.js +1 -1
  121. package/{migrations → src/migrations}/410300_transferExtension.js +1 -1
  122. package/{migrations → src/migrations}/410302_transferExtension-transferErrorId.js +1 -1
  123. package/{migrations → src/migrations}/410350_bulkTransferExtension.js +1 -1
  124. package/{migrations → src/migrations}/410400_transferTimeout.js +1 -1
  125. package/{migrations → src/migrations}/410450_transferErrorDuplicateCheck.js +1 -1
  126. package/{migrations → src/migrations}/410500_transferError.js +1 -1
  127. package/{migrations → src/migrations}/410502_transferError-foreignTransferErrorDuplicateCheckId.js +1 -1
  128. package/{migrations → src/migrations}/410550_bulkTransferError.js +1 -1
  129. package/{migrations → src/migrations}/410600_expiringTransfer.js +1 -1
  130. package/{migrations → src/migrations}/500401_quote-hotfix-2719.js +8 -8
  131. package/{migrations → src/migrations}/500501_feature-fixSubIdRef.js +12 -12
  132. package/{migrations → src/migrations}/500601_party-2480.js +3 -3
  133. package/{migrations → src/migrations}/501001_quoteExtension-quote-service-174.js +1 -2
  134. package/{migrations → src/migrations}/501002_quoteExtension-2522.js +3 -3
  135. package/{migrations → src/migrations}/600010_fxTransferType.js +1 -1
  136. package/{migrations → src/migrations}/600012_fxParticipantCurrencyType.js +1 -1
  137. package/{migrations → src/migrations}/600100_fxTransferDuplicateCheck.js +2 -2
  138. package/{migrations → src/migrations}/600110_fxTransferErrorDuplicateCheck.js.js +1 -1
  139. package/{migrations → src/migrations}/600200_fxTransfer.js +2 -2
  140. package/{migrations → src/migrations}/600400_fxTransferStateChange.js +1 -1
  141. package/{migrations → src/migrations}/600501_fxWatchList.js +2 -2
  142. package/{migrations → src/migrations}/600600_fxTransferFulfilmentDuplicateCheck.js +1 -1
  143. package/{migrations → src/migrations}/600700_fxTransferFulfilment.js +1 -1
  144. package/{migrations → src/migrations}/600800_fxTransferExtension.js +1 -1
  145. package/{migrations → src/migrations}/601400_fxTransferTimeout.js +1 -1
  146. package/{migrations → src/migrations}/601500_fxTransferError.js +1 -1
  147. package/{migrations → src/migrations}/610200_fxTransferParticipant.js +1 -1
  148. package/{migrations → src/migrations}/610202_fxTransferParticipant-participantId.js +2 -2
  149. package/{migrations → src/migrations}/610403_participantPositionChange-fxTransfer.js +1 -1
  150. package/{migrations → src/migrations}/800100_quotePartyView.js +1 -1
  151. package/{migrations → src/migrations}/800101_feature-fixSubIdRef.js +1 -1
  152. package/{migrations → src/migrations}/800200_quoteView.js +1 -1
  153. package/{migrations → src/migrations}/800300_quoteResponseView.js +1 -1
  154. package/{migrations → src/migrations}/900100_foreign-keys.js +18 -18
  155. package/{migrations → src/migrations}/900101_foreign-keys.js +6 -6
  156. package/{migrations → src/migrations}/910100_feature904DDL.js +7 -7
  157. package/{migrations → src/migrations}/910101_feature904DataMigration.js +1 -1
  158. package/{migrations → src/migrations}/910102_feature949DataMigration.js +1 -1
  159. package/{migrations → src/migrations}/910103_feature1093SettlementByCurrency.js +3 -3
  160. package/{migrations → src/migrations}/940101_quotePartyIdInfoExtension-2522.js +25 -25
  161. package/{migrations → src/migrations}/950101_transferParticipantStateChange.js +2 -2
  162. package/{migrations → src/migrations}/950102_settlementModel-adjustPosition.js +2 -2
  163. package/{migrations → src/migrations}/950103_dropTransferParticipantStateChange.js +3 -3
  164. package/{migrations → src/migrations}/950104_settlementModel-settlementAccountTypeId.js +3 -3
  165. package/src/migrations/950107_settlementWindowContent_foreign_settlementModel.js +63 -0
  166. package/{migrations → src/migrations}/950108_participantProxy.js +1 -2
  167. package/{migrations → src/migrations}/950120_fxQuoteConversionTerms_sourceAmount_nullable.js +1 -1
  168. package/{migrations → src/migrations}/960100_create_externalParticipant.js +2 -2
  169. package/{migrations → src/migrations}/960110_alter_transferParticipant__addFiled_externalParticipantId.js +2 -2
  170. package/{migrations → src/migrations}/961001_transferForwarded.js +1 -1
  171. package/{migrations → src/migrations}/961002_fxTransferForwarded.js +1 -1
  172. package/src/models/participant/externalParticipant.int.ts +85 -0
  173. package/src/models/participant/facade.js +4 -2
  174. package/src/models/participant/participantCurrencyCached.js +1 -1
  175. package/src/models/position/facade.js +8 -2
  176. package/src/models/transfer/facade.int.ts +139 -0
  177. package/src/models/transfer/facade.js +8 -3
  178. package/src/models/transfer/transferDuplicateCheck.js +2 -2
  179. package/src/models/transfer/transferError.int.ts +112 -0
  180. package/src/models/transfer/transferExtension.int.ts +137 -0
  181. package/src/models/transfer/transferStateChange.int.ts +117 -0
  182. package/{seeds → src/seeds}/participant.js +1 -1
  183. package/{seeds → src/seeds}/settlementInterchange.js +1 -0
  184. package/src/settlement/api/handlerRoutes.js +71 -0
  185. package/src/settlement/api/handlers/health.js +10 -0
  186. package/src/settlement/api/handlers/index.js +57 -0
  187. package/src/settlement/api/handlers/settlementWindows/{id}.js +103 -0
  188. package/src/settlement/api/handlers/settlementWindows.js +77 -0
  189. package/src/settlement/api/handlers/settlements/{id}.js +133 -0
  190. package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}/accounts/{aid}.js +127 -0
  191. package/src/settlement/api/handlers/settlements/{sid}/participants/{pid}.js +127 -0
  192. package/src/settlement/api/handlers/settlements.js +114 -0
  193. package/src/settlement/api/openapiRouting.js +87 -0
  194. package/src/settlement/api/routes.js +176 -0
  195. package/src/settlement/domain/rules/index.js +46 -0
  196. package/src/settlement/domain/settlement/index.js +405 -0
  197. package/src/settlement/domain/settlementWindow/index.js +117 -0
  198. package/src/settlement/domain/transactions/index.js +83 -0
  199. package/src/settlement/domain/transferSettlement/index.js +57 -0
  200. package/src/settlement/handlers/deferredSettlement/handler.js +180 -0
  201. package/src/settlement/handlers/grossSettlement/handler.js +180 -0
  202. package/src/settlement/handlers/index.js +101 -0
  203. package/src/settlement/handlers/register.js +92 -0
  204. package/src/settlement/handlers/rules/handler.js +164 -0
  205. package/src/settlement/interface/swagger-handler.json +34 -0
  206. package/src/settlement/interface/swagger.json +1560 -0
  207. package/src/settlement/lib/db.js +32 -0
  208. package/src/settlement/lib/kafka/index.js +41 -0
  209. package/src/settlement/lib/kafka/producer.js +142 -0
  210. package/src/settlement/lib/scriptEngine.js +99 -0
  211. package/src/settlement/lib/scriptsLoader.js +175 -0
  212. package/src/settlement/lib/utility.js +243 -0
  213. package/src/settlement/models/ilpPackets/ilpPacket.js +42 -0
  214. package/src/settlement/models/lib/enums.js +192 -0
  215. package/src/settlement/models/misc/migrationLock.js +53 -0
  216. package/src/settlement/models/settlement/facade.js +1652 -0
  217. package/src/settlement/models/settlement/index.js +55 -0
  218. package/src/settlement/models/settlement/participantCurrency.js +48 -0
  219. package/src/settlement/models/settlement/settlement.js +49 -0
  220. package/src/settlement/models/settlement/settlementModel.js +44 -0
  221. package/src/settlement/models/settlement/settlementParticipantCurrency.js +61 -0
  222. package/src/settlement/models/settlement/settlementStateChange.js +44 -0
  223. package/src/settlement/models/settlement/settlementTransferParticipant.js +46 -0
  224. package/src/settlement/models/settlementWindow/facade.js +345 -0
  225. package/src/settlement/models/settlementWindow/index.js +47 -0
  226. package/src/settlement/models/settlementWindow/settlementWindowStateChange.js +55 -0
  227. package/src/settlement/models/settlementWindowContent/facade.js +79 -0
  228. package/src/settlement/models/settlementWindowContent/index.js +40 -0
  229. package/src/settlement/models/settlementWindowContent/settlementWindowContentStateChange.js +53 -0
  230. package/src/settlement/models/transferSettlement/facade.js +363 -0
  231. package/src/settlement/models/transferSettlement/index.js +42 -0
  232. package/src/settlement/shared/logger/index.js +7 -0
  233. package/src/settlement/shared/plugins.js +88 -0
  234. package/src/settlement/shared/setup.js +224 -0
  235. package/src/settlement/utils/truthyProperty.js +37 -0
  236. package/src/shared/legacyUtilShim.js +20 -0
  237. package/src/shared/plugins.js +3 -3
  238. package/src/shared/results.ts +22 -0
  239. package/src/shared/results.unit.ts +38 -0
  240. package/src/shared/setup.js +14 -1
  241. package/src/testing/api-helpers.ts +1222 -0
  242. package/src/testing/exec-async.ts +67 -0
  243. package/src/testing/harness.snapshot.sql +3895 -0
  244. package/src/testing/harness.test.ts +15 -0
  245. package/src/testing/harness.ts +1160 -0
  246. package/src/testing/kafka.ts +221 -0
  247. package/src/testing/prng.ts +82 -0
  248. package/src/testing/rdkafka.test.ts +263 -0
  249. package/src/testing/run.ts +559 -0
  250. package/src/testing/snapshot.test.ts +92 -0
  251. package/src/testing/snapshot.ts +549 -0
  252. package/src/testing/tap-stream.ts +164 -0
  253. package/src/testing/types.ts +95 -0
  254. package/src/testing/util.ts +333 -0
  255. package/src/types/central-services-error-handling.d.ts +72 -0
  256. package/src/types/central-services-stream.d.ts +148 -0
  257. package/test-integration.Dockerfile +0 -2
  258. package/test.Dockerfile +0 -2
  259. package/tsconfig.json +40 -0
  260. package/config/knexfile.js +0 -19
  261. package/migrations/950107_settlementWindowContent_foreign_settlementModel.js +0 -63
  262. package/src/api/index.js +0 -16
  263. package/src/lib/config.js +0 -81
  264. /package/{migrations → src/migrations}/110101_contactType-indexes.js +0 -0
  265. /package/{migrations → src/migrations}/110301_endpointType-indexes.js +0 -0
  266. /package/{migrations → src/migrations}/110401_ledgerEntryType-indexes.js +0 -0
  267. /package/{migrations → src/migrations}/110451_ledgerAccountType-indexes.js +0 -0
  268. /package/{migrations → src/migrations}/110501_participantLimitType-indexes.js +0 -0
  269. /package/{migrations → src/migrations}/110601_transferParticipantRoleType-indexes.js +0 -0
  270. /package/{migrations → src/migrations}/110800_settlementWindowState.js +0 -0
  271. /package/{migrations → src/migrations}/111000_amountType.js +0 -0
  272. /package/{migrations → src/migrations}/111001_amountType-indexes.js +0 -0
  273. /package/{migrations → src/migrations}/111100_balanceOfPayments.js +0 -0
  274. /package/{migrations → src/migrations}/111101_balanceOfPayments-indexes.js +0 -0
  275. /package/{migrations → src/migrations}/111200_partyIdentifierType.js +0 -0
  276. /package/{migrations → src/migrations}/111201_partyIdentifierType-indexes.js +0 -0
  277. /package/{migrations → src/migrations}/111300_partyType.js +0 -0
  278. /package/{migrations → src/migrations}/111301_partyType-indexes.js +0 -0
  279. /package/{migrations → src/migrations}/111400_quoteDuplicateCheck.js +0 -0
  280. /package/{migrations → src/migrations}/111500_transactionInitiator.js +0 -0
  281. /package/{migrations → src/migrations}/111501_transactionInitiator-indexes.js +0 -0
  282. /package/{migrations → src/migrations}/111600_transactionInitiatorType.js +0 -0
  283. /package/{migrations → src/migrations}/111601_transactionInitiatorType-indexes.js +0 -0
  284. /package/{migrations → src/migrations}/111700_settlementDelay.js +0 -0
  285. /package/{migrations → src/migrations}/111701_settlementDelay-indexes.js +0 -0
  286. /package/{migrations → src/migrations}/111800_settlementInterchange.js +0 -0
  287. /package/{migrations → src/migrations}/111801_settlementInterchange-indexes.js +0 -0
  288. /package/{migrations → src/migrations}/111900_settlementGranularity.js +0 -0
  289. /package/{migrations → src/migrations}/111901_settlementGranularity-indexes.js +0 -0
  290. /package/{migrations → src/migrations}/112101_bulkProcessingState-indexes.js +0 -0
  291. /package/{migrations → src/migrations}/200101_participant-indexes.js +0 -0
  292. /package/{migrations → src/migrations}/200201_participantContact-indexes.js +0 -0
  293. /package/{migrations → src/migrations}/200301_participantEndpoint-indexes.js +0 -0
  294. /package/{migrations → src/migrations}/200401_participantParty-indexes.js +0 -0
  295. /package/{migrations → src/migrations}/300201_transfer-indexes.js +0 -0
  296. /package/{migrations → src/migrations}/300251_bulkTransfer-indexes.js +0 -0
  297. /package/{migrations → src/migrations}/300276_bulkTransferAssociation-indexes.js +0 -0
  298. /package/{migrations → src/migrations}/300401_transferStateChange-indexes.js +0 -0
  299. /package/{migrations → src/migrations}/300451_bulkTransferStateChange-indexes.js +0 -0
  300. /package/{migrations → src/migrations}/300501_segment-indexes.js +0 -0
  301. /package/{migrations → src/migrations}/310101_participantCurrency-indexes.js +0 -0
  302. /package/{migrations → src/migrations}/310201_transferParticipant-indexes.js +0 -0
  303. /package/{migrations → src/migrations}/310203_transferParticipant-indexes.js +0 -0
  304. /package/{migrations → src/migrations}/310301_participantPosition-indexes.js +0 -0
  305. /package/{migrations → src/migrations}/310401_participantPositionChange-indexes.js +0 -0
  306. /package/{migrations → src/migrations}/310405_participantPositionChange-positionChange.js +0 -0
  307. /package/{migrations → src/migrations}/310406_participantPositionChange-positionChange-revert.js +0 -0
  308. /package/{migrations → src/migrations}/310501_participantLimit-indexes.js +0 -0
  309. /package/{migrations → src/migrations}/310503_participantLimit-indexes-composite.js +0 -0
  310. /package/{migrations → src/migrations}/400401_settlmentWindowStateChange-indexes.js +0 -0
  311. /package/{migrations → src/migrations}/400501_settlementTransferParticipant-indexes.js +0 -0
  312. /package/{migrations → src/migrations}/400601_settlementParticipantCurrency-indexes.js +0 -0
  313. /package/{migrations → src/migrations}/400701_settlementParticipantCurrencyStateChange-indexes.js +0 -0
  314. /package/{migrations → src/migrations}/400801_settlementStateChange-indexes.js +0 -0
  315. /package/{migrations → src/migrations}/401002_settlementWindowContent-indexes.js +0 -0
  316. /package/{migrations → src/migrations}/401201_settlementContentAggregation-indexes.js +0 -0
  317. /package/{migrations → src/migrations}/401301_settlementModel-indexes.js +0 -0
  318. /package/{migrations → src/migrations}/410101_transferFulfilmentDuplicateCheck-indexes.js +0 -0
  319. /package/{migrations → src/migrations}/410201_transferFulfilment-indexes.js +0 -0
  320. /package/{migrations → src/migrations}/410301_transferExtension-indexes.js +0 -0
  321. /package/{migrations → src/migrations}/410351_bulkTransferExtension-indexes.js +0 -0
  322. /package/{migrations → src/migrations}/410401_transferTimeout-indexes.js +0 -0
  323. /package/{migrations → src/migrations}/410501_transferError-indexes.js +0 -0
  324. /package/{migrations → src/migrations}/410551_bulkTransferError-indexes.js +0 -0
  325. /package/{migrations → src/migrations}/410601_expiringTransfer-indexes.js +0 -0
  326. /package/{migrations → src/migrations}/500100_transactionReference.js +0 -0
  327. /package/{migrations → src/migrations}/500101_transactionReference-indexes.js +0 -0
  328. /package/{migrations → src/migrations}/500200_transactionScenario.js +0 -0
  329. /package/{migrations → src/migrations}/500201_transactionScenario-indexes.js +0 -0
  330. /package/{migrations → src/migrations}/500300_transactionSubScenario.js +0 -0
  331. /package/{migrations → src/migrations}/500400_quote.js +0 -0
  332. /package/{migrations → src/migrations}/500500_quoteParty.js +0 -0
  333. /package/{migrations → src/migrations}/500600_party.js +0 -0
  334. /package/{migrations → src/migrations}/500700_quoteResponse.js +0 -0
  335. /package/{migrations → src/migrations}/500800_quoteResponseIlpPacket.js +0 -0
  336. /package/{migrations → src/migrations}/500900_geoCode.js +0 -0
  337. /package/{migrations → src/migrations}/501000_quoteExtension.js +0 -0
  338. /package/{migrations → src/migrations}/501100_quoteResponseDuplicateCheck.js +0 -0
  339. /package/{migrations → src/migrations}/501200_quoteError.js +0 -0
  340. /package/{migrations → src/migrations}/501300_transferRules.js +0 -0
  341. /package/{migrations → src/migrations}/600011_fxTransferType-indexes.js +0 -0
  342. /package/{migrations → src/migrations}/600013_fxParticipantCurrencyType-indexes.js +0 -0
  343. /package/{migrations → src/migrations}/600201_fxTransfer-indexes.js +0 -0
  344. /package/{migrations → src/migrations}/600401_fxTransferStateChange-indexes.js +0 -0
  345. /package/{migrations → src/migrations}/600502_fxWatchList-indexes.js +0 -0
  346. /package/{migrations → src/migrations}/600601_fxTransferFulfilmentDuplicateCheck-indexes.js +0 -0
  347. /package/{migrations → src/migrations}/600701_fxTransferFulfilment-indexes.js +0 -0
  348. /package/{migrations → src/migrations}/601401_fxTransferTimeout-indexes.js +0 -0
  349. /package/{migrations → src/migrations}/601501_fxTransferError-indexes.js +0 -0
  350. /package/{migrations → src/migrations}/610201_fxTransferParticipant-indexes.js +0 -0
  351. /package/{migrations → src/migrations}/910102_hotfix954.js +0 -0
  352. /package/{migrations → src/migrations}/940100_quotePartyIdInfoExtension.js +0 -0
  353. /package/{migrations → src/migrations}/950100_settlementContentAggregation-decimal184.js +0 -0
  354. /package/{migrations → src/migrations}/950106_unique_settlement_model_ledger_account_currency.js +0 -0
  355. /package/{migrations → src/migrations}/950109_fxQuote.js +0 -0
  356. /package/{migrations → src/migrations}/950110_fxQuoteResponse.js +0 -0
  357. /package/{migrations → src/migrations}/950111_fxQuoteError.js +0 -0
  358. /package/{migrations → src/migrations}/950113_fxQuoteDuplicateCheck.js +0 -0
  359. /package/{migrations → src/migrations}/950114_fxQuoteResponseDuplicateCheck.js +0 -0
  360. /package/{migrations → src/migrations}/950115_fxQuoteConversionTerms.js +0 -0
  361. /package/{migrations → src/migrations}/950116_fxQuoteConversionTermsExtension.js +0 -0
  362. /package/{migrations → src/migrations}/950117_fxQuoteResponseConversionTerms.js +0 -0
  363. /package/{migrations → src/migrations}/950118_fxQuoteResponseConversionTermsExtension.js +0 -0
  364. /package/{migrations → src/migrations}/950119_fxCharge.js +0 -0
  365. /package/{migrations → src/migrations}/960111_alter_fxTransferParticipant__addFiled_externalParticipantId.js +0 -0
  366. /package/{migrations → src/migrations}/960112_fxTransfer-indexes2.js +0 -0
  367. /package/{migrations → src/migrations}/960113_fxTransferExtension-indexes.js +0 -0
  368. /package/{migrations → src/migrations}/960114_fxTransferTimeout-indexes2.js +0 -0
  369. /package/{migrations → src/migrations}/961000_participantEndpoint_unique.js +0 -0
  370. /package/{migrations → src/migrations}/migration.template +0 -0
  371. /package/{seeds → src/seeds}/amountType.js +0 -0
  372. /package/{seeds → src/seeds}/balanceOfPayments.js +0 -0
  373. /package/{seeds → src/seeds}/bulkProcessingState.js +0 -0
  374. /package/{seeds → src/seeds}/bulkTransferState.js +0 -0
  375. /package/{seeds → src/seeds}/currency.js +0 -0
  376. /package/{seeds → src/seeds}/endpointType.js +0 -0
  377. /package/{seeds → src/seeds}/fxParticipantCurrencyType.js +0 -0
  378. /package/{seeds → src/seeds}/fxTransferType.js +0 -0
  379. /package/{seeds → src/seeds}/ledgerAccountType.js +0 -0
  380. /package/{seeds → src/seeds}/ledgerEntryType.js +0 -0
  381. /package/{seeds → src/seeds}/participantLimitType.js +0 -0
  382. /package/{seeds → src/seeds}/partyIdentifierType.js +0 -0
  383. /package/{seeds → src/seeds}/partyType.js +0 -0
  384. /package/{seeds → src/seeds}/settlementDelay.js +0 -0
  385. /package/{seeds → src/seeds}/settlementGranularity.js +0 -0
  386. /package/{seeds → src/seeds}/settlementState.js +0 -0
  387. /package/{seeds → src/seeds}/settlementWindow1State.js +0 -0
  388. /package/{seeds → src/seeds}/settlementWindow2Open.js +0 -0
  389. /package/{seeds → src/seeds}/transactionInitiator.js +0 -0
  390. /package/{seeds → src/seeds}/transactionInitiatorType.js +0 -0
  391. /package/{seeds → src/seeds}/transactionScenario.js +0 -0
  392. /package/{seeds → src/seeds}/transferParticipantRoleType.js +0 -0
  393. /package/{seeds → src/seeds}/transferState.js +0 -0
  394. /package/{seeds → src/seeds}/z1000_settlementModel-deprecated.js- +0 -0
@@ -0,0 +1,1160 @@
1
+ /*****
2
+ License
3
+ --------------
4
+ Copyright © 2020-2026 Mojaloop Foundation
5
+ The Mojaloop files are made available by the Mojaloop Foundation under the Apache License, Version 2.0 (the "License") and you may not use these files except in compliance with the License. You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, the Mojaloop files are distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
10
+
11
+ Contributors
12
+ --------------
13
+ This is the official list of the Mojaloop project contributors for this file.
14
+ Names of the original copyright holders (individuals or organizations)
15
+ should be listed with a '*' in the first column. People who have
16
+ contributed from an organization can be listed under the organization
17
+ that actually holds the copyright for their contributions (see the
18
+ Mojaloop Foundation for an example). Those individuals should have
19
+ their names indented and be marked with a '-'. Email address can be added
20
+ optionally within square brackets <email>.
21
+
22
+ * TigerBeetle
23
+ - Lewis Daly <lewis@tigerbeetle.com>
24
+ --------------
25
+
26
+ ******/
27
+
28
+ // Unfortunately each Winston logger created by @mojaloop/central-services-logger registers it's own
29
+ // uncaughtException handler. There's no easy way to pass through a common logger around, so we set
30
+ // the maxListeners to ~13 to avoid the `MaxListenersExceededWarning`.
31
+ process.setMaxListeners(13)
32
+
33
+ // Disable the Event SDK logs globally.
34
+ const EventSdkConfig = require('@mojaloop/event-sdk/dist/lib/config')
35
+ EventSdkConfig.default.EVENT_LOGGER_LOG_FILTER = ''
36
+ const EventSdk = require('@mojaloop/event-sdk')
37
+ const span = EventSdk.Tracer.createSpan('thing')
38
+ span.audit('Hopefully we dont see this!')
39
+
40
+ import assert from "assert"
41
+ import { execAsync } from "./exec-async"
42
+
43
+ import Cache from '../lib/cache'
44
+ import { makeConfig } from "../lib/config/resolver"
45
+ import { deepMerge } from "../lib/config/util"
46
+ import Db from '../lib/db'
47
+ import Enums from '../lib/enumCached'
48
+ import { Enum } from '@mojaloop/central-services-shared'
49
+
50
+ const Metrics = require('@mojaloop/central-services-metrics')
51
+
52
+ // Note: we _must_ use `require()` here, otherwise the global Producer and Consumers are imported
53
+ // as empty objects.
54
+ const KafkaProducer = require('@mojaloop/central-services-stream').Util.Producer
55
+ const KafkaConsumer = require('@mojaloop/central-services-stream').Util.Consumer
56
+ const Utility = require('@mojaloop/central-services-shared').Util.Kafka
57
+
58
+ import AdminHandler from '../handlers/admin/handler'
59
+ import PositionHandler from '../handlers/positions/handler'
60
+ import PositionBatchHandler from '../handlers/positions/handlerBatch'
61
+
62
+ import ParticipantCached from '../models/participant/participantCached'
63
+ import ParticipantCurrencyCached from '../models/participant/participantCurrencyCached'
64
+ import ParticipantLimitCached from '../models/participant/participantLimitCached'
65
+ import ProxyCache from '../lib/proxyCache'
66
+ const BatchPositionModelCached = require('../models/position/batchCached')
67
+ const ExternalParticipantCached = require('../models/participant/externalParticipantCached')
68
+
69
+ import Logger from "@mojaloop/central-services-logger"
70
+ import knex from 'knex'
71
+ import { ApplicationConfig, overrideForTesting, RecursivePartial, resetOverride } from "../lib/config"
72
+ import { randomAvailablePort } from "./util"
73
+ import { Consumer } from "./kafka"
74
+ import { Message } from "node-rdkafka"
75
+
76
+ const logger = Logger.child({ scope: 'harness' })
77
+
78
+ export interface HarnessOptions {
79
+ /**
80
+ * A unique id used in naming and logs to disambiguate between multiple harness runs.
81
+ */
82
+ id: number
83
+ }
84
+
85
+ /**
86
+ * @class Harness
87
+ *
88
+ * @description Testing harness used to manage the dependencies for running e2e and integration.
89
+ * ```
90
+ * import Harness from './harness
91
+ * const harness = harness.getInstance()
92
+ * ```
93
+ *
94
+ * Then, in the before() and after() hooks:
95
+ * ```
96
+ * before(async () => {
97
+ * await harness.up()
98
+ * await harness.setupGlobals()
99
+ * })
100
+ *
101
+ * after(async () => {
102
+ * await harness.teardownGlobals()
103
+ * await harness.down()
104
+ * })
105
+ * ```
106
+ *
107
+ * Depends on:
108
+ * 1. Docker host for `mysql`, `kafka`, `redis` containers.
109
+ * 2. Filesystem access for `tigerbeetle` binary.
110
+ *
111
+ * The goal is that the integration tests are responsible for setting up and tearing down their
112
+ * own environment.
113
+ */
114
+ export default class Harness {
115
+ private static instance: Harness | null = null;
116
+ private options: HarnessOptions
117
+ private dependencyRedpanda: Redpanda
118
+ private dependencyMySql: MySql
119
+ private dependencyRedis: Redis
120
+ private applicationConfig: ApplicationConfig | null = null;
121
+ private omniConsumer: Consumer | null = null;
122
+ private messageQueue: Array<MojaloopKafkaMessage> = []
123
+ private positionHandlerType: 'NON_BATCH' | 'BATCH' = 'NON_BATCH'
124
+
125
+ /**
126
+ *
127
+ * There's 2 types of enums:
128
+ * 1. Statically defined enums in types.
129
+ * 2. Enums we need get from the database and pass around for some reason (I really don't know
130
+ * _why_).
131
+ *
132
+ * We keep a reference to #2 here for convenience.
133
+ */
134
+ private _enums: any = null;
135
+
136
+ private constructor(options: HarnessOptions) {
137
+ this.options = options
138
+
139
+ this.dependencyRedpanda = new Redpanda({
140
+ harnessId: this.options.id
141
+ })
142
+
143
+ this.dependencyMySql = new MySql({
144
+ harnessId: this.options.id,
145
+ databaseName: 'central_ledger',
146
+ migration: {
147
+ type: 'sql',
148
+ sqlFilePath: './src/testing/harness.snapshot.sql',
149
+
150
+ // Uncomment below to update the harness.snapshot.sql file.
151
+ // You'll want to do this after adding new migrations.
152
+ // type: 'knex',
153
+ // updateSqlFilePath: './src/testing/harness.snapshot.sql'
154
+ }
155
+ })
156
+
157
+ this.dependencyRedis = new Redis({
158
+ harnessId: this.options.id
159
+ })
160
+ }
161
+
162
+ public static getInstance(): Harness {
163
+ if (!Harness.instance) {
164
+ let run = Math.floor(Math.random() * (100000 - 10000) + 10000)
165
+ if (process.env.RUN) {
166
+ try {
167
+ run = Number.parseInt(process.env.RUN)
168
+ } catch (err: any) {
169
+ throw new Error(`Invalid test run id. process.env.RUN should be an integer.`)
170
+ }
171
+ }
172
+
173
+ Harness.instance = new Harness({
174
+ id: run,
175
+ })
176
+ }
177
+ return Harness.instance;
178
+ }
179
+
180
+ /**
181
+ * The BATCH position handler contains a bug which makes it unable to properly process
182
+ * aborted transfers. Since we will be soon removing the position handlers, we work around
183
+ * this bug by allowing the harness to specify whether or not to use the NON_BATCH or BATCH
184
+ * position handler for a specific test.
185
+ *
186
+ * The bug is related to cyrilResult not being set for a non-fx transfer, which causes the
187
+ * position rest message to not be fired.
188
+ */
189
+ public async up(positionHandlerType: 'NON_BATCH' | 'BATCH' = 'NON_BATCH') {
190
+ const start = performance.now()
191
+ await this.checkEnvironment()
192
+
193
+ const results = await Promise.allSettled([
194
+ this.dependencyRedpanda.up(),
195
+ this.dependencyMySql.up(),
196
+ this.dependencyRedis.up(),
197
+ ])
198
+
199
+ let failed = false
200
+ let failedCount = 0
201
+ let errorMessage = ''
202
+ results.forEach(result => {
203
+ if (result.status === 'rejected') {
204
+ failed = true
205
+ failedCount += 1
206
+ errorMessage += `${result.reason}\n`
207
+ }
208
+ })
209
+
210
+ if (failed) {
211
+ logger.error(`Harness.up() encountered: ${failedCount} failure${failed === 1 ? '' : 's'}`)
212
+ logger.error(`Detailed errors: ${errorMessage}`)
213
+ throw new Error(`Harness.up() encountered: ${failedCount} failures.`)
214
+ }
215
+
216
+ const defaultConfig = makeConfig()
217
+ const kafkaBroker = `localhost:${this.dependencyRedpanda.connectionOptions.port}`
218
+
219
+ // Shortcut to make below more bearable.
220
+ const innerKafkaConfig = {
221
+ config: {
222
+ rdkafkaConf: {
223
+ "metadata.broker.list": kafkaBroker
224
+ }
225
+ }
226
+ }
227
+
228
+ this.positionHandlerType = positionHandlerType
229
+ let positionHandlerOverrides: Record<string, string> = {
230
+ PREPARE: 'topic-transfer-position',
231
+ COMMIT: 'topic-transfer-position',
232
+ RESERVE: 'topic-transfer-position',
233
+ TIMEOUT_RESERVED: 'topic-transfer-position',
234
+ ABORT: 'topic-transfer-position'
235
+ }
236
+
237
+ if (positionHandlerType === 'BATCH') {
238
+ positionHandlerOverrides = {
239
+ PREPARE: 'topic-transfer-position-batch',
240
+ FX_PREPARE: 'topic-transfer-position-batch',
241
+ COMMIT: 'topic-transfer-position-batch',
242
+ RESERVE: 'topic-transfer-position-batch',
243
+ FX_RESERVE: 'topic-transfer-position-batch',
244
+ TIMEOUT_RESERVED: 'topic-transfer-position-batch',
245
+ FX_TIMEOUT_RESERVED: 'topic-transfer-position-batch',
246
+ ABORT: 'topic-transfer-position-batch',
247
+ FX_ABORT: 'topic-transfer-position-batch',
248
+ }
249
+ }
250
+
251
+ // Override the config based on the harness variables.
252
+ const override: RecursivePartial<ApplicationConfig> = {
253
+ PROXY_CACHE_CONFIG: {
254
+ enabled: true,
255
+ type: 'redis',
256
+ proxyConfig: {
257
+ host: 'localhost',
258
+ port: this.dependencyRedis.connectionOptions.port,
259
+ }
260
+ },
261
+ DATABASE: {
262
+ connection: {
263
+ user: 'root',
264
+ port: this.dependencyMySql.connectionOptions.port,
265
+ }
266
+ },
267
+ KAFKA_CONFIG: {
268
+ EVENT_TYPE_ACTION_TOPIC_MAP: {
269
+ POSITION: positionHandlerOverrides,
270
+ },
271
+ CONSUMER: {
272
+ ADMIN: {
273
+ TRANSFER: innerKafkaConfig,
274
+ },
275
+ TRANSFER: {
276
+ PREPARE: innerKafkaConfig,
277
+ FULFIL: innerKafkaConfig,
278
+ // TODO: remove this when we finish fusing the handlers.
279
+ POSITION: innerKafkaConfig,
280
+ POSITION_BATCH: innerKafkaConfig,
281
+ },
282
+ NOTIFICATION: {
283
+ EVENT: innerKafkaConfig
284
+ },
285
+ DEFERREDSETTLEMENT: {
286
+ CLOSE: innerKafkaConfig
287
+ }
288
+ },
289
+ PRODUCER: {
290
+ ADMIN: {
291
+ TRANSFER: innerKafkaConfig
292
+ },
293
+ TRANSFER: {
294
+ PREPARE: innerKafkaConfig,
295
+ FULFIL: innerKafkaConfig,
296
+ // TODO: remove this when we finish fusing the handlers.
297
+ POSITION: innerKafkaConfig,
298
+ },
299
+ NOTIFICATION: {
300
+ EVENT: innerKafkaConfig
301
+ }
302
+ }
303
+ }
304
+ }
305
+ this.applicationConfig = deepMerge(defaultConfig, override)
306
+
307
+ this.omniConsumer = new Consumer('omniconsumer', kafkaBroker)
308
+ await this.omniConsumer?.subscribe([
309
+ 'topic-transfer-prepare',
310
+ 'topic-transfer-fulfil',
311
+ 'topic-transfer-position',
312
+ 'topic-transfer-position-batch',
313
+ 'topic-notification-event',
314
+ 'topic-admin-transfer',
315
+ ], (message) => {
316
+ if (message.topic === '__consumer_offsets') {
317
+ return
318
+ }
319
+
320
+ try {
321
+ this.appendMessageQueue(message)
322
+ } catch (err: any) {
323
+ console.error('Failed to append message to queue:\n')
324
+ console.error(err.message)
325
+ }
326
+ })
327
+
328
+ const end = performance.now()
329
+ logger.warn(`Harness.up() took: ${(end - start).toFixed(0)} ms.`)
330
+ }
331
+
332
+ get mySqlConnectionOptions(): MySqlConnectionOptions {
333
+ return this.dependencyMySql.connectionOptions
334
+ }
335
+
336
+ get redpandaConnectionOptions(): RedpandaConnectionOptions {
337
+ return this.dependencyRedpanda.connectionOptions
338
+ }
339
+
340
+ get config(): ApplicationConfig {
341
+ assert(this.applicationConfig, 'No this.applicationConfig. Did you forget to call up()?')
342
+ return this.applicationConfig
343
+ }
344
+
345
+ get enums(): any {
346
+ assert(this._enums, 'Enums not initalized. Did you forget to call setupGlobals()?')
347
+ return this._enums
348
+ }
349
+
350
+ get topicTransferPrepare(): { topicName: string } {
351
+ return Utility.createGeneralTopicConf(
352
+ this.config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE,
353
+ Enum.Events.Event.Type.TRANSFER,
354
+ Enum.Events.Event.Type.PREPARE
355
+ )
356
+ }
357
+
358
+ get topicTransferFulfil(): { topicName: string } {
359
+ return Utility.createGeneralTopicConf(
360
+ this.config.KAFKA_CONFIG.TOPIC_TEMPLATES.GENERAL_TOPIC_TEMPLATE.TEMPLATE,
361
+ Enum.Events.Event.Type.TRANSFER,
362
+ Enum.Events.Event.Type.FULFIL,
363
+ )
364
+ }
365
+
366
+ private appendMessageQueue(message: Message): void {
367
+ assert(message)
368
+ assert(message.value)
369
+ const messageValueStr = message.value.toString()
370
+ const parsed = JSON.parse(messageValueStr)
371
+ assert(parsed)
372
+
373
+ assert(message.timestamp, 'message.timestamp is not defined.')
374
+
375
+ const mojaloopKafkaMessage = {
376
+ ...message,
377
+ valueStr: messageValueStr,
378
+ valueParsed: parsed
379
+ } as MojaloopKafkaMessage
380
+ const lastMessage = this.peekMessageQueue()
381
+ const lastTimestamp = lastMessage ? lastMessage.timestamp : 0
382
+ // Even if the message is outdated, still append to the message queue instead of dropping it.
383
+ // This warning will help us catch tests that have overlapping messages.
384
+ if (mojaloopKafkaMessage.timestamp < lastTimestamp) {
385
+ const message = `appendMessageQueue() inserted a stale message with timestamp:\
386
+ ${mojaloopKafkaMessage.timestamp} after message with timestamp: ${lastTimestamp}.`
387
+ logger.warn(message)
388
+ }
389
+ this.messageQueue.push(mojaloopKafkaMessage)
390
+ }
391
+
392
+ /**
393
+ * Get the last message from the queue.
394
+ */
395
+ private peekMessageQueue(): MojaloopKafkaMessage | undefined {
396
+ if (this.messageQueue.length === 0) {
397
+ return
398
+ }
399
+ return this.messageQueue.at(-1)
400
+ }
401
+
402
+ /**
403
+ * Legacy central-ledger code uses a lot of globals everywhere. This is a convenience function
404
+ * so we don't have to call this at the start of each test.
405
+ */
406
+ public async setupGlobals(): Promise<void> {
407
+ logger.info('setupGlobals()')
408
+ // Override the global config with our testing config.
409
+ overrideForTesting(this.config)
410
+
411
+ await Db.connect(this.config.DATABASE)
412
+ await ParticipantCached.initialize()
413
+ await ParticipantCurrencyCached.initialize()
414
+ await BatchPositionModelCached.initialize()
415
+ ExternalParticipantCached.initialize()
416
+
417
+ await ParticipantLimitCached.initialize()
418
+ await Cache.initCache()
419
+ Metrics.setup(this.config.INSTRUMENTATION_METRICS_CONFIG)
420
+
421
+ Enums.initialize()
422
+ this._enums = await Enums.getEnums('all')
423
+
424
+ // Register the `topic-transfer-position` consumer
425
+ // Because the kafka registration uses global scope, we cannot register both the non batch
426
+ // and batch position handlers, in spite of the fact that they have different topics.
427
+ switch (this.positionHandlerType) {
428
+ case 'NON_BATCH':
429
+ await PositionHandler.registerPositionHandler()
430
+ break;
431
+ case 'BATCH':
432
+ await PositionBatchHandler.registerPositionHandler()
433
+ break;
434
+ }
435
+
436
+ await AdminHandler.registerAllHandlers()
437
+ }
438
+
439
+ public async teardownGlobals(): Promise<void> {
440
+ try {
441
+ logger.info('teardownGlobals()')
442
+ await ProxyCache.disconnect()
443
+ await Cache.destroyCache()
444
+ await Db.disconnect()
445
+ await KafkaProducer.disconnect()
446
+ await KafkaConsumer.disconnectAll()
447
+ resetOverride()
448
+ } catch (err: any) {
449
+ logger.error(`teardownGlobals() failed with error: ${err.message}`)
450
+ throw err
451
+ }
452
+ }
453
+
454
+ private async checkEnvironment() {
455
+ try {
456
+ // Check that docker is installed.
457
+ await execAsync(`docker --version`)
458
+ } catch (err: any) {
459
+ logger.error(`command: 'docker --version' failed. Ensure docker is installed in this
460
+ environment!\n ${err.message}`)
461
+ throw new Error('checkEnvironment() failed.')
462
+ }
463
+
464
+ try {
465
+ // Check that we have a docker daemon running.
466
+ await execAsync(`docker ps`)
467
+ } catch (err: any) {
468
+ logger.error(`command: 'docker ps' failed. Is the docker daemon running?\n ${err.message}`)
469
+ throw new Error('checkEnvironment() failed.')
470
+ }
471
+ }
472
+
473
+ public async down() {
474
+ let forceExit = false
475
+ const start = performance.now()
476
+ logger.warn(`harness.down()`)
477
+
478
+ if (this.omniConsumer) {
479
+ try {
480
+ await this.omniConsumer.disconnect()
481
+ } catch (err: any) {
482
+ forceExit = true
483
+ }
484
+ }
485
+
486
+ const results = await Promise.allSettled([
487
+ this.dependencyRedpanda.down(),
488
+ this.dependencyMySql.down(),
489
+ this.dependencyRedis.down(),
490
+ ])
491
+
492
+ let failed = false
493
+ let failedCount = 0
494
+ let errorMessage = ''
495
+ results.forEach(result => {
496
+ if (result.status === 'rejected') {
497
+ failed = true
498
+ failedCount += 1
499
+ errorMessage += `${result.reason}\n`
500
+ }
501
+ })
502
+
503
+ if (failed) {
504
+ logger.error(`Harness.down() encountered: ${failedCount} failure${failed === 1 ? '' : 's'}`)
505
+ logger.error(`Detailed errors: ${errorMessage}`)
506
+ throw new Error(`Harness.down() encountered: ${failedCount} failures.`)
507
+ }
508
+
509
+ const end = performance.now()
510
+ logger.warn(`Harness.down() took: ${(end - start).toFixed(0)} ms.`)
511
+
512
+ if (forceExit) {
513
+ logger.warn('force exiting')
514
+ setTimeout(() => process.kill(process.pid, 'SIGKILL'), 4000);
515
+ }
516
+ }
517
+
518
+ public redpandaMark(): number {
519
+ return this.messageQueue.length
520
+ }
521
+
522
+ /**
523
+ * @description Wait for redpanda to produce and consume _n_ messages.
524
+ */
525
+ public async redpandaDrain(markLast: number, numMessages: number, attempts: number = 25): Promise<void> {
526
+ const start = performance.now()
527
+ assert(markLast >= 0)
528
+ assert(numMessages >= 0)
529
+
530
+ let delayMs = 10
531
+ let markNew = this.messageQueue.length
532
+ for (let attempt = 1; attempt <= attempts; attempt++) {
533
+ try {
534
+ markNew = this.messageQueue.length
535
+ if (markNew < markLast) {
536
+ throw new Error(`It appears redpanda went backwards! markLast: ${markLast} --> ${markNew}`)
537
+ }
538
+
539
+ if (markNew > (markLast + numMessages)) {
540
+ const errorMessage = `Redpanda expected to consume: ${numMessages}, but consumed: ${markNew - markLast}`
541
+ logger.error(errorMessage)
542
+ this.printLast(markNew - markLast)
543
+ throw new Error(errorMessage)
544
+ }
545
+
546
+ if (markNew === (markLast + numMessages)) {
547
+ const end = performance.now()
548
+ logger.info(`Redpanda consumed ${numMessages} message${numMessages === 1 ? ' ' : 's'} after ${(end - start).toFixed(0).padStart(4)}ms.`)
549
+
550
+ // Cool down for 20ms, check that there are no late messages.
551
+ await new Promise(resolve => setTimeout(resolve, 20))
552
+ const extraMessages = this.messageQueue.length - markNew
553
+ assert(extraMessages >= 0)
554
+ if (extraMessages !== 0) {
555
+ const errorMessage = `After cooldown, Redpanda consumed ${extraMessages} extra message${extraMessages === 1 ? ' ' : 's'}.`
556
+ logger.error(errorMessage)
557
+
558
+ this.printLast(numMessages + extraMessages)
559
+ throw new Error(errorMessage)
560
+ }
561
+
562
+ return
563
+ }
564
+
565
+ throw new Error('Not ready')
566
+ } catch (err: any) {
567
+ if (attempt === attempts) {
568
+ const errorMessage = `redpandaDrain() failed to consume ${numMessages} messages after ${attempts} attempts.\
569
+ Found only ${markNew - markLast} new messages.`
570
+ logger.error(errorMessage)
571
+ this.printLast(markNew - markLast)
572
+
573
+ throw new Error(errorMessage)
574
+ }
575
+
576
+ if (err.message !== 'Not ready') {
577
+ logger.error(err.message)
578
+ throw err
579
+ }
580
+
581
+ logger.info(`redpandaDrain() waiting for Redpanda: [attempt ${`${attempt}`.padStart(3)}/${attempts}, delayMs: ${delayMs}].`)
582
+ // Slowly back off.
583
+ delayMs = Math.floor((delayMs * 1.1) + 10)
584
+ await new Promise(resolve => setTimeout(resolve, delayMs))
585
+ }
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Get the complete message of the last _n_ messages produced across all topics.
591
+ */
592
+ public spoolLast(numMessages: number): Array<MojaloopKafkaMessage> {
593
+ assert(numMessages > 0)
594
+
595
+ if (numMessages > this.messageQueue.length) {
596
+ throw new Error(`spoolLast() requested: ${numMessages}, but messageQueue only has: ${this.messageQueue.length} messages.`)
597
+ }
598
+
599
+ const last = this.messageQueue.slice(numMessages * -1)
600
+ assert(last.length === numMessages)
601
+ return last
602
+ }
603
+
604
+ /**
605
+ * @description Print the last messages in the messageQueue. If `numMessages` is undefined, prints
606
+ * all messages.
607
+ */
608
+ public printLast(numMessages?: number): void {
609
+ let messages = this.spoolLast(this.messageQueue.length)
610
+ if (numMessages) {
611
+ assert(numMessages >= 0)
612
+ assert(numMessages <= messages.length)
613
+ messages = messages.slice(numMessages * -1)
614
+ }
615
+ logger.warn('printLast() messages:')
616
+ messages.forEach(msg => {
617
+ logger.warn(`\n
618
+ ts: ${msg.timestamp}
619
+ topic: ${msg.topic}
620
+ uriParams: ${msg.valueParsed.content.uriParams ?
621
+ JSON.stringify(msg.valueParsed.content.uriParams) : ''
622
+ }
623
+ valueParsed:
624
+ ${JSON.stringify(msg.valueParsed.content.payload, null, 2)}
625
+ `.replaceAll(/^\s{6}/gm, ''))
626
+ })
627
+ }
628
+
629
+ /**
630
+ * Sometimes we just want to check the last topics that were published to.
631
+ */
632
+ public spoolLastTopic(numMessages: number): Array<string> {
633
+ const last = this.spoolLast(numMessages)
634
+ return last.map(message => message.topic)
635
+ }
636
+
637
+ /**
638
+ * Get the payload of the last _n_ messages produced across all topics.
639
+ */
640
+ public spoolLastPayload(numMessages: number): Array<any> {
641
+ const last = this.spoolLast(numMessages)
642
+ return last.map(message => message.valueParsed.content.payload)
643
+ }
644
+ }
645
+
646
+ interface DependencyOptions {
647
+ harnessId: number,
648
+ }
649
+
650
+ interface RedpandaConnectionOptions {
651
+ port: number
652
+ }
653
+
654
+ export type MojaloopKafkaMessage = {
655
+ topic: string,
656
+ valueStr: string,
657
+ timestamp: number,
658
+ partition: number,
659
+ offset: number,
660
+ valueParsed: {
661
+ content: {
662
+ uriParams: any
663
+ context: any,
664
+ headers: any,
665
+ payload: any,
666
+ },
667
+ metadata: {
668
+ event: any,
669
+ 'protocol.createdAt': number,
670
+ trace: any
671
+ }
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Starts a redpanda and redpanda console docker container.
677
+ * Redpanda is much quicker to start up than Kafka, so is more suitable for this testing harness.
678
+ */
679
+ class Redpanda {
680
+ private logger = logger.child({ scope: 'Redpanda' })
681
+ private options: DependencyOptions
682
+ private containerName: string
683
+ private containerNameConsole: string
684
+ private _connectionOptions: null | RedpandaConnectionOptions
685
+
686
+ private topics = [
687
+ 'topic-transfer-prepare',
688
+ 'topic-transfer-position',
689
+ 'topic-transfer-fulfil',
690
+ 'topic-notification-event',
691
+ 'topic-admin-transfer',
692
+ 'topic-transfer-position-batch',
693
+ ]
694
+
695
+ constructor(options: DependencyOptions) {
696
+ assert(options)
697
+ assert(options.harnessId)
698
+
699
+ this.options = options;
700
+ this.containerName = `int_${this.options.harnessId}_redpanda`
701
+ this.containerNameConsole = `int_${this.options.harnessId}_redpanda_console`
702
+ this._connectionOptions = null
703
+ }
704
+
705
+ public async up(): Promise<void> {
706
+ this.logger.debug(`up()`)
707
+ const portRedpanda = await randomAvailablePort()
708
+ const portConsole = await randomAvailablePort()
709
+
710
+ const command = `
711
+ docker rm -f ${this.containerName} ${this.containerNameConsole} 2>/dev/null;
712
+ docker network create harness || echo 'harness exists';
713
+ docker run -d \
714
+ --name ${this.containerName} \
715
+ --network harness \
716
+ -p ${portRedpanda}:9092 \
717
+ --health-cmd="rpk cluster info" \
718
+ --health-interval=1s \
719
+ --health-timeout=2s \
720
+ --health-retries=100 \
721
+ --health-start-period=2s \
722
+ docker.io/redpandadata/redpanda:latest \
723
+ redpanda start \
724
+ --smp 1 \
725
+ --memory 400M \
726
+ --reserve-memory 0M \
727
+ --overprovisioned \
728
+ --node-id 0 \
729
+ --check=false \
730
+ --kafka-addr internal://0.0.0.0:29092,external://0.0.0.0:9092 \
731
+ --advertise-kafka-addr internal://${this.containerName}:29092,external://localhost:${portRedpanda}
732
+ `.replace(/\s+/g, ' ')
733
+ const { stdout, stderr } = await execAsync(command)
734
+ this.logger.info(`Redpanda.up() stdout: ${stdout}`)
735
+ this.logger.info(`Redpanda.up() stderr: ${stderr}`)
736
+
737
+ const commandConsole = `
738
+ docker run -d \
739
+ --name ${this.containerNameConsole} \
740
+ --hostname ${this.containerNameConsole} \
741
+ --restart on-failure \
742
+ --network harness \
743
+ -p ${portConsole}:8080 \
744
+ -e KAFKA_BROKERS=${this.containerName}:29092 \
745
+ docker.redpanda.com/redpandadata/console:latest
746
+ `.replace(/\s+/g, ' ')
747
+
748
+ await execAsync(commandConsole)
749
+
750
+ this._connectionOptions = {
751
+ port: portRedpanda,
752
+ }
753
+
754
+ this.logger.warn(`Redpanda - go to: http://localhost:${portConsole} to see the Redpanda Console`);
755
+ await this.waitForHealthy()
756
+ await this.createTopics()
757
+ this.logger.debug(`up() - Complete.`)
758
+ }
759
+
760
+ private async waitForHealthy(): Promise<void> {
761
+ assert(this._connectionOptions)
762
+
763
+ let attemptsMax = 75
764
+ let delayMs = 50
765
+
766
+ for (let attempt = 1; attempt <= attemptsMax; attempt++) {
767
+ try {
768
+ const command = `docker inspect --format='{{.State.Health.Status}}' ${this.containerName}`
769
+ const { stdout } = await execAsync(command, { silent: true })
770
+
771
+ if (stdout.trim() !== 'healthy') {
772
+ throw new Error('Not ready.')
773
+ }
774
+
775
+ logger.warn(`Redpanda started after ${attempt} attempts (${attempt * delayMs}ms).`)
776
+ return
777
+ } catch (err: any) {
778
+ if (attempt === attemptsMax) {
779
+ throw new Error(`Redpanda failed to start after ${attemptsMax} attempts.\n${err.message}`)
780
+ }
781
+
782
+ logger.debug(`Waiting for Redpanda: [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
783
+ await new Promise(resolve => setTimeout(resolve, delayMs))
784
+ }
785
+ }
786
+ }
787
+
788
+ private async createTopics(): Promise<void> {
789
+ logger.debug(`createTopics() - creating ${this.topics.length} kafka topics`);
790
+
791
+ const commands = this.topics.map(topic =>
792
+ `docker exec ${this.containerName} rpk topic create ${topic}`
793
+ );
794
+
795
+ await Promise.all(commands.map(async cmd => {
796
+ await execAsync(cmd, { silent: true, force: true })
797
+ }))
798
+ }
799
+
800
+ get connectionOptions(): MySqlConnectionOptions {
801
+ if (!this._connectionOptions) {
802
+ throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
803
+ }
804
+ return this._connectionOptions
805
+ }
806
+
807
+ public async down(): Promise<void> {
808
+ this.logger.debug(`down() - stopping and removing containers: ${this.containerName}, ${this.containerNameConsole}.`)
809
+ try {
810
+ await execAsync(`docker stop ${this.containerName} ${this.containerNameConsole}`, { silent: true })
811
+ await execAsync(`docker rm -f ${this.containerName} ${this.containerNameConsole}`, { silent: true })
812
+ this.logger.debug(`down() - Complete.`)
813
+ } catch (err: any) {
814
+ this.logger.error(`down() - failed to remove containers: ${err.message}`)
815
+ throw err
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Get the sum of all watermarks across all topics.
821
+ */
822
+ public async mark(): Promise<number> {
823
+ let watermarkSum = 0
824
+ for (const topic of this.topics) {
825
+ const cmd = `docker exec ${this.containerName} rpk topic describe ${topic} --format=json`
826
+ const { stdout } = await execAsync(cmd)
827
+
828
+ const describeJson = JSON.parse(stdout)[0].partitions[0]
829
+ watermarkSum += describeJson.high_watermark
830
+ }
831
+ return watermarkSum;
832
+ }
833
+ }
834
+
835
+ interface DependencyOptionsMySql extends DependencyOptions {
836
+ databaseName: string,
837
+ migration: MigrationOptions
838
+ }
839
+
840
+ interface MigrationOptionsKnex {
841
+ type: 'knex';
842
+
843
+ /**
844
+ * If this is set, then after running the knex migration, perform a mysql dump
845
+ * to update the migration file
846
+ */
847
+ updateSqlFilePath?: string
848
+ }
849
+
850
+ interface MigrationOptionsSql {
851
+ type: 'sql';
852
+ sqlFilePath: string;
853
+ }
854
+
855
+ type MigrationOptions = MigrationOptionsKnex | MigrationOptionsSql;
856
+
857
+ interface MySqlConnectionOptions {
858
+ port: number
859
+ }
860
+
861
+ class MySql {
862
+ private logger = logger.child({ scope: 'MySql' })
863
+ private options: DependencyOptionsMySql
864
+ private containerName: string
865
+ private _connectionOptions: MySqlConnectionOptions | null
866
+
867
+ constructor(options: DependencyOptionsMySql) {
868
+ assert(options)
869
+ assert(options.harnessId)
870
+
871
+ this.options = options;
872
+ this.containerName = `int_${this.options.harnessId}_mysql`
873
+ this._connectionOptions = null
874
+ }
875
+
876
+ public async up(): Promise<void> {
877
+ this.logger.debug(`up()`)
878
+ const port = await randomAvailablePort()
879
+
880
+ const command = `
881
+ docker rm -f ${this.containerName} 2>/dev/null;
882
+ docker run -d \
883
+ --name ${this.containerName} \
884
+ --tmpfs /var/lib/mysql:rw,size=256m \
885
+ -e MARIADB_ROOT_PASSWORD=password \
886
+ -e MARIADB_DATABASE=${this.options.databaseName} \
887
+ -p ${port}:3306 \
888
+ mariadb:latest
889
+ `.replace(/\s/g, ' ')
890
+ await execAsync(command)
891
+
892
+ this.logger.info(`MySql starting at localhost:${port}`);
893
+
894
+ this._connectionOptions = { port }
895
+ await this.waitForMySqlReady()
896
+ await this.migrate()
897
+ await this.seed()
898
+
899
+ this.logger.debug(`up() - Complete.`)
900
+ }
901
+
902
+ get connectionOptions(): MySqlConnectionOptions {
903
+ if (!this._connectionOptions) {
904
+ throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
905
+ }
906
+ return this._connectionOptions
907
+ }
908
+
909
+ public async down(): Promise<void> {
910
+ this.logger.debug(`down() - stopping and removing containers: ${this.containerName}.`)
911
+ try {
912
+ await execAsync(`docker stop ${this.containerName}`, { silent: true })
913
+ await execAsync(`docker rm -f ${this.containerName}`, { silent: true })
914
+ this.logger.debug(`down() - Complete.`)
915
+ } catch (err: any) {
916
+ this.logger.error(`down() - failed to remove containers: ${err.message}`)
917
+ throw err
918
+ }
919
+ }
920
+
921
+ /**
922
+ * Call exec on the container to make sure mysql is ready for connections.
923
+ *
924
+ * The probe goes over TCP rather than the unix socket on purpose. The mariadb entrypoint
925
+ * bootstraps the database on a temporary server started with `--skip-networking`, then stops
926
+ * it and starts the real one. A socket probe passes against that temporary server and then
927
+ * migrate() races its shutdown - `ERROR 2002 ... Can't connect to local server through socket`.
928
+ * Only the real server listens on 3306, so a TCP probe cannot succeed too early.
929
+ */
930
+ private async waitForMySqlReady(): Promise<void> {
931
+ assert(this._connectionOptions)
932
+
933
+ let attemptsMax = 150
934
+ let delayMs = 35
935
+
936
+ for (let attempt = 1; attempt <= attemptsMax; attempt++) {
937
+ try {
938
+ const command = `docker exec ${this.containerName} sh -c \
939
+ 'mariadb --protocol=TCP -h 127.0.0.1 -P 3306 -u root -ppassword -e "select 1" ${this.options.databaseName}'
940
+ `
941
+ await execAsync(command)
942
+ logger.warn(`MySql started after ${attempt} attempts (${attempt * delayMs}ms).`)
943
+ return
944
+ } catch (err: any) {
945
+ if (attempt === attemptsMax) {
946
+ throw new Error(`MySql failed to start after ${attemptsMax} attempts.\n${err.message}`)
947
+ }
948
+ // Extra whitespace for better printing.
949
+ logger.debug(`Waiting for MySQL: [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
950
+ await new Promise(resolve => setTimeout(resolve, delayMs))
951
+ }
952
+ }
953
+ }
954
+
955
+ private async migrate(): Promise<void> {
956
+ assert(this._connectionOptions)
957
+
958
+ // Sometimes migration fails even if MySQL is ready, so we wrap this in retries.
959
+ let attemptsMax = 3
960
+ let delayMs = 1000
961
+
962
+ for (let attempt = 1; attempt <= attemptsMax; attempt++) {
963
+ try {
964
+ const type = this.options.migration.type
965
+ switch (type) {
966
+ case "knex":
967
+ await this.migrateKnex()
968
+ break;
969
+ case "sql":
970
+ await this.migrateSql()
971
+ break
972
+ default:
973
+ throw new Error(`Unexpected migration type: ${type}`)
974
+ }
975
+ return
976
+ } catch (err: any) {
977
+ if (attempt === attemptsMax) {
978
+ throw new Error(`migrate failed after ${attemptsMax}.\n${err.message}`)
979
+ }
980
+ logger.debug(`migrate() [attempt ${`${attempt}`.padStart(3)}/${attemptsMax}]`)
981
+ await new Promise(resolve => setTimeout(resolve, delayMs))
982
+ }
983
+ }
984
+ }
985
+
986
+ private getKnexClient() {
987
+ return knex({
988
+ client: 'mysql2',
989
+ connection: {
990
+ host: 'localhost',
991
+ port: this.connectionOptions.port,
992
+ user: 'root',
993
+ password: 'password',
994
+ database: this.options.databaseName,
995
+ },
996
+ migrations: {
997
+ tableName: 'migration',
998
+ directory: './src/migrations'
999
+ },
1000
+ seeds: {
1001
+ directory: './src/seeds'
1002
+ }
1003
+ })
1004
+ }
1005
+
1006
+ /**
1007
+ * @method migrateKnex()
1008
+ * @description Runs all of the knex migrations. It's quite slow so you probably want to use
1009
+ * migrateSql() to restore the databse from the checkpoint sql file, and check that file into
1010
+ * git.
1011
+ */
1012
+ private async migrateKnex(): Promise<void> {
1013
+ assert(this.options.migration.type === 'knex')
1014
+
1015
+ const knexClient = this.getKnexClient();
1016
+ try {
1017
+ await knexClient.migrate.latest()
1018
+ logger.debug('migrateKnex() - complete.')
1019
+
1020
+ if (this.options.migration.updateSqlFilePath) {
1021
+ await this.saveDatabaseCheckpoint(this.options.migration.updateSqlFilePath)
1022
+ }
1023
+ } finally {
1024
+ await knexClient.destroy()
1025
+ }
1026
+ }
1027
+
1028
+ private async saveDatabaseCheckpoint(pathToCheckpoint: string): Promise<void> {
1029
+ try {
1030
+ logger.info(`saveDatabaseCheckpoint() - creating checkpoint at: ${pathToCheckpoint}`)
1031
+
1032
+ // Dump the database inside the container to a known location.
1033
+ const containerTempFile = '/tmp/checkpoint_dump.sql'
1034
+ const dumpCmd = `docker exec ${this.containerName} sh -c \
1035
+ 'mariadb-dump -u root -ppassword ${this.options.databaseName} > ${containerTempFile}'`;
1036
+ const { stderr: dumpStderr } = await execAsync(dumpCmd);
1037
+
1038
+ if (dumpStderr && !dumpStderr.includes('Warning: Using a password')) {
1039
+ logger.warn('SQL dump warnings:', dumpStderr);
1040
+ }
1041
+
1042
+ const copyCmd = `docker cp ${this.containerName}:${containerTempFile} ${pathToCheckpoint}`;
1043
+ const { stderr: copyStderr } = await execAsync(copyCmd);
1044
+
1045
+ if (copyStderr) {
1046
+ logger.warn('Docker copy warnings:', copyStderr);
1047
+ }
1048
+
1049
+ const addNoticeCmd = `echo "-- Note: This file was generated by ./testing/harness.ts.
1050
+ -- It is used to speed up the integration tests, but needs to be recreated every time a new
1051
+ -- migration is added. Refer to ./src/testing/harness.ts for instructions of how to update this
1052
+ -- snapshot.
1053
+ " > /tmp/checkpoint; \
1054
+ cat ${pathToCheckpoint} >> /tmp/checkpoint; \
1055
+ `.replace(/\ {2,}/g, '')
1056
+ await execAsync(addNoticeCmd, { silent: false })
1057
+
1058
+ await execAsync(`mv /tmp/checkpoint ${pathToCheckpoint}`)
1059
+ logger.info(`SQL checkpoint saved to ${pathToCheckpoint}`)
1060
+
1061
+ } catch (err: any) {
1062
+ logger.error(`saveDatabaseCheckpoint() failed with error: ${err.message}`)
1063
+ throw err
1064
+ }
1065
+ }
1066
+
1067
+ private async migrateSql(): Promise<void> {
1068
+ assert(this.options.migration.type === 'sql')
1069
+
1070
+ try {
1071
+ logger.debug(`migrateSql(): from: ${this.options.migration.sqlFilePath}.`)
1072
+ const cmd = `docker cp ${this.options.migration.sqlFilePath} ${this.containerName}:/tmp/checkpoint.sql && \
1073
+ docker exec -i ${this.containerName} sh -c 'mariadb -u root -ppassword ${this.options.databaseName} < /tmp/checkpoint.sql'
1074
+ `
1075
+ const { stdout, stderr } = await execAsync(cmd);
1076
+
1077
+ if (stderr && !stderr.includes('warning')) {
1078
+ logger.debug('migrateSql() warnings:', stderr);
1079
+ }
1080
+
1081
+ logger.debug(`migrateSql() from ${this.options.migration.sqlFilePath} completed`);
1082
+ } catch (err: any) {
1083
+ throw new Error(`migrateSql() from: ${this.options.migration.sqlFilePath} failed with error: ${err.message}.`)
1084
+ }
1085
+ }
1086
+
1087
+ private async seed(): Promise<void> {
1088
+ const knexClient = this.getKnexClient();
1089
+ try {
1090
+ await knexClient.seed.run()
1091
+ logger.debug('seed() - complete.')
1092
+ } finally {
1093
+ await knexClient.destroy()
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ interface DependencyOptionsRedis extends DependencyOptions {
1099
+ }
1100
+
1101
+ interface RedisConnectionOptions {
1102
+ port: number
1103
+ }
1104
+
1105
+ class Redis {
1106
+ private logger = logger.child({ scope: 'Redis' })
1107
+ private containerName: string
1108
+ private _connectionOptions: RedisConnectionOptions | null
1109
+
1110
+ constructor(private options: DependencyOptionsRedis) {
1111
+ assert(options)
1112
+ assert(options.harnessId)
1113
+
1114
+ this.containerName = `int_${this.options.harnessId}_redis`
1115
+ this._connectionOptions = null
1116
+ }
1117
+
1118
+ public async up(): Promise<void> {
1119
+ this.logger.debug(`up()`)
1120
+ const port = await randomAvailablePort()
1121
+
1122
+ const command = `
1123
+ docker rm -f ${this.containerName} 2>/dev/null;
1124
+ docker run -d \
1125
+ --name ${this.containerName} \
1126
+ -p ${port}:6379 \
1127
+ -e ALLOW_EMPTY_PASSWORD=yes \
1128
+ --health-cmd "redis-cli ping" \
1129
+ --health-timeout 2s \
1130
+ --health-interval 10s \
1131
+ redis:latest
1132
+ `.replace(/\s/g, ' ')
1133
+ await execAsync(command)
1134
+
1135
+ this.logger.info(`Redis starting at localhost:${port}`);
1136
+
1137
+ this._connectionOptions = { port }
1138
+
1139
+ this.logger.debug(`up() - Complete.`)
1140
+ }
1141
+
1142
+ get connectionOptions() {
1143
+ if (!this._connectionOptions) {
1144
+ throw new Error(`this._connectionOptions is null. Did you forget to call up()?`)
1145
+ }
1146
+ return this._connectionOptions
1147
+ }
1148
+
1149
+ public async down(): Promise<void> {
1150
+ this.logger.debug(`down() - stopping and removing containers: ${this.containerName}.`)
1151
+ try {
1152
+ await execAsync(`docker stop ${this.containerName}`, { silent: true })
1153
+ await execAsync(`docker rm -f ${this.containerName}`, { silent: true })
1154
+ this.logger.debug(`down() - Complete.`)
1155
+ } catch (err: any) {
1156
+ this.logger.error(`down() - failed to remove containers: ${err.message}`)
1157
+ throw err
1158
+ }
1159
+ }
1160
+ }