@intlayer/backend 8.12.2 → 8.12.4-canary.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 (415) hide show
  1. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
  2. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
  3. package/dist/esm/controllers/ai.controller.mjs.map +1 -1
  4. package/dist/esm/controllers/audit.controller.mjs.map +1 -1
  5. package/dist/esm/controllers/bitbucket.controller.mjs.map +1 -1
  6. package/dist/esm/controllers/cliSessionToken.controller.mjs.map +1 -1
  7. package/dist/esm/controllers/demo.controller.mjs +32 -25
  8. package/dist/esm/controllers/demo.controller.mjs.map +1 -1
  9. package/dist/esm/controllers/dictionary.controller.mjs +1 -0
  10. package/dist/esm/controllers/dictionary.controller.mjs.map +1 -1
  11. package/dist/esm/controllers/environment.controller.mjs.map +1 -1
  12. package/dist/esm/controllers/eventListener.controller.mjs.map +1 -1
  13. package/dist/esm/controllers/github.controller.mjs.map +1 -1
  14. package/dist/esm/controllers/gitlab.controller.mjs.map +1 -1
  15. package/dist/esm/controllers/newsletter.controller.mjs.map +1 -1
  16. package/dist/esm/controllers/oAuth2.controller.mjs.map +1 -1
  17. package/dist/esm/controllers/organization.controller.mjs.map +1 -1
  18. package/dist/esm/controllers/project.controller.mjs.map +1 -1
  19. package/dist/esm/controllers/projectAccessKey.controller.mjs.map +1 -1
  20. package/dist/esm/controllers/projectMemberAccess.controller.mjs.map +1 -1
  21. package/dist/esm/controllers/recursiveAudit.controller.mjs.map +1 -1
  22. package/dist/esm/controllers/reviewer.controller.mjs.map +1 -1
  23. package/dist/esm/controllers/searchDoc.controller.mjs.map +1 -1
  24. package/dist/esm/controllers/showcaseProject.controller.mjs.map +1 -1
  25. package/dist/esm/controllers/stripe.controller.mjs.map +1 -1
  26. package/dist/esm/controllers/tag.controller.mjs.map +1 -1
  27. package/dist/esm/controllers/translation.controller.mjs.map +1 -1
  28. package/dist/esm/controllers/user.controller.mjs.map +1 -1
  29. package/dist/esm/emails/AffiliateActivatedEmail.mjs.map +1 -1
  30. package/dist/esm/emails/AffiliateConversionEmail.mjs.map +1 -1
  31. package/dist/esm/emails/AffiliateInvitationEmail.mjs.map +1 -1
  32. package/dist/esm/emails/AffiliateWelcomeEmail.mjs.map +1 -1
  33. package/dist/esm/emails/InviteUserEmail.mjs.map +1 -1
  34. package/dist/esm/emails/MagicLinkEmail.mjs.map +1 -1
  35. package/dist/esm/emails/MissionRequestedClientEmail.mjs.map +1 -1
  36. package/dist/esm/emails/MissionRequestedReviewerEmail.mjs.map +1 -1
  37. package/dist/esm/emails/OAuthTokenCreatedEmail.mjs.map +1 -1
  38. package/dist/esm/emails/PasswordChangeConfirmation.mjs.map +1 -1
  39. package/dist/esm/emails/ResetUserPassword.mjs.map +1 -1
  40. package/dist/esm/emails/ReviewerApplicationEmail.mjs.map +1 -1
  41. package/dist/esm/emails/ReviewerApprovedEmail.mjs.map +1 -1
  42. package/dist/esm/emails/ReviewerContactEmail.mjs.map +1 -1
  43. package/dist/esm/emails/SubscriptionPaymentCancellation.mjs.map +1 -1
  44. package/dist/esm/emails/SubscriptionPaymentError.mjs.map +1 -1
  45. package/dist/esm/emails/SubscriptionPaymentSuccess.mjs.map +1 -1
  46. package/dist/esm/emails/ValidateUserEmail.mjs.map +1 -1
  47. package/dist/esm/emails/Welcome.mjs.map +1 -1
  48. package/dist/esm/index.mjs +1 -1
  49. package/dist/esm/index.mjs.map +1 -1
  50. package/dist/esm/logger/index.mjs.map +1 -1
  51. package/dist/esm/middlewares/oAuth2.middleware.mjs.map +1 -1
  52. package/dist/esm/middlewares/sessionAuth.middleware.mjs.map +1 -1
  53. package/dist/esm/routes/ai.routes.mjs.map +1 -1
  54. package/dist/esm/routes/audit.routes.mjs.map +1 -1
  55. package/dist/esm/routes/bitbucket.routes.mjs.map +1 -1
  56. package/dist/esm/routes/demo.routes.mjs.map +1 -1
  57. package/dist/esm/routes/dictionary.routes.mjs.map +1 -1
  58. package/dist/esm/routes/environment.routes.mjs.map +1 -1
  59. package/dist/esm/routes/eventListener.routes.mjs.map +1 -1
  60. package/dist/esm/routes/github.routes.mjs.map +1 -1
  61. package/dist/esm/routes/gitlab.routes.mjs.map +1 -1
  62. package/dist/esm/routes/newsletter.routes.mjs.map +1 -1
  63. package/dist/esm/routes/organization.routes.mjs.map +1 -1
  64. package/dist/esm/routes/paramsSchemas.mjs.map +1 -1
  65. package/dist/esm/routes/project.routes.mjs.map +1 -1
  66. package/dist/esm/routes/reviewer.routes.mjs.map +1 -1
  67. package/dist/esm/routes/search.routes.mjs.map +1 -1
  68. package/dist/esm/routes/showcaseProject.routes.mjs.map +1 -1
  69. package/dist/esm/routes/stripe.routes.mjs.map +1 -1
  70. package/dist/esm/routes/tags.routes.mjs.map +1 -1
  71. package/dist/esm/routes/translate.routes.mjs.map +1 -1
  72. package/dist/esm/routes/user.routes.mjs.map +1 -1
  73. package/dist/esm/schemas/account.schema.mjs.map +1 -1
  74. package/dist/esm/schemas/affiliate.schema.mjs.map +1 -1
  75. package/dist/esm/schemas/affiliateInvitation.schema.mjs.map +1 -1
  76. package/dist/esm/schemas/audit.schema.mjs.map +1 -1
  77. package/dist/esm/schemas/auditJob.schema.mjs.map +1 -1
  78. package/dist/esm/schemas/auditPage.schema.mjs.map +1 -1
  79. package/dist/esm/schemas/cliSessionToken.schema.mjs.map +1 -1
  80. package/dist/esm/schemas/dictionary.schema.mjs.map +1 -1
  81. package/dist/esm/schemas/discussion.schema.mjs.map +1 -1
  82. package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
  83. package/dist/esm/schemas/organization.schema.mjs.map +1 -1
  84. package/dist/esm/schemas/plans.schema.mjs.map +1 -1
  85. package/dist/esm/schemas/project.schema.mjs.map +1 -1
  86. package/dist/esm/schemas/promoCode.schema.mjs.map +1 -1
  87. package/dist/esm/schemas/reviewer.schema.mjs.map +1 -1
  88. package/dist/esm/schemas/session.schema.mjs.map +1 -1
  89. package/dist/esm/schemas/showcaseProject.schema.mjs.map +1 -1
  90. package/dist/esm/schemas/tag.schema.mjs.map +1 -1
  91. package/dist/esm/schemas/user.schema.mjs.map +1 -1
  92. package/dist/esm/services/affiliate.service.mjs.map +1 -1
  93. package/dist/esm/services/audit/analysis/analyzeBundleContent.mjs.map +1 -1
  94. package/dist/esm/services/audit/analysis/analyzeLinguisticStructure.mjs.map +1 -1
  95. package/dist/esm/services/audit/analysis/analyzeMetadata.mjs.map +1 -1
  96. package/dist/esm/services/audit/analysis/analyzeRobots.mjs.map +1 -1
  97. package/dist/esm/services/audit/analysis/analyzeSitemap.mjs.map +1 -1
  98. package/dist/esm/services/audit/analysis/analyzeUrlStructure.mjs.map +1 -1
  99. package/dist/esm/services/audit/analysis/calculateScore.mjs.map +1 -1
  100. package/dist/esm/services/audit/checkers/bundleChecker.mjs.map +1 -1
  101. package/dist/esm/services/audit/checkers/linguisticChecker.mjs.map +1 -1
  102. package/dist/esm/services/audit/checkers/metadataChecker.mjs.map +1 -1
  103. package/dist/esm/services/audit/checkers/pageChecker.mjs.map +1 -1
  104. package/dist/esm/services/audit/checkers/robotsChecker.mjs.map +1 -1
  105. package/dist/esm/services/audit/checkers/sitemapChecker.mjs.map +1 -1
  106. package/dist/esm/services/audit/checkers/urlChecker.mjs.map +1 -1
  107. package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
  108. package/dist/esm/services/audit/seoAudit.service.mjs.map +1 -1
  109. package/dist/esm/services/bitbucket.service.mjs.map +1 -1
  110. package/dist/esm/services/ci.service.mjs.map +1 -1
  111. package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
  112. package/dist/esm/services/dictionary.service.mjs.map +1 -1
  113. package/dist/esm/services/email.service.mjs.map +1 -1
  114. package/dist/esm/services/environment.service.mjs.map +1 -1
  115. package/dist/esm/services/github.service.mjs.map +1 -1
  116. package/dist/esm/services/gitlab.service.mjs.map +1 -1
  117. package/dist/esm/services/oAuth2.service.mjs.map +1 -1
  118. package/dist/esm/services/organization.service.mjs.map +1 -1
  119. package/dist/esm/services/project/projectScreenshot.service.mjs.map +1 -1
  120. package/dist/esm/services/project.service.mjs.map +1 -1
  121. package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
  122. package/dist/esm/services/promoCode.service.mjs.map +1 -1
  123. package/dist/esm/services/reviewer/pictureUpload.service.mjs.map +1 -1
  124. package/dist/esm/services/reviewer.service.mjs.map +1 -1
  125. package/dist/esm/services/reviewerMessage.service.mjs.map +1 -1
  126. package/dist/esm/services/reviewerMission.service.mjs.map +1 -1
  127. package/dist/esm/services/session.service.mjs.map +1 -1
  128. package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
  129. package/dist/esm/services/showcase/showcaseScan.service.mjs.map +1 -1
  130. package/dist/esm/services/showcase/showcaseUploadScreenshot.service.mjs.map +1 -1
  131. package/dist/esm/services/showcase/showcaseVerifyBundle.service.mjs.map +1 -1
  132. package/dist/esm/services/showcase/showcaseVerifyGithub.service.mjs.map +1 -1
  133. package/dist/esm/services/subscription.service.mjs.map +1 -1
  134. package/dist/esm/services/tag.service.mjs.map +1 -1
  135. package/dist/esm/services/translationQueue.service.mjs.map +1 -1
  136. package/dist/esm/services/translationWorker.service.mjs.map +1 -1
  137. package/dist/esm/services/user/avatarUpload.service.mjs.map +1 -1
  138. package/dist/esm/services/user.service.mjs.map +1 -1
  139. package/dist/esm/services/webhook.service.mjs.map +1 -1
  140. package/dist/esm/types/user.types.mjs.map +1 -1
  141. package/dist/esm/utils/AI/askDocQuestion/askDocQuestion.mjs.map +1 -1
  142. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/bundle_optimization.json +9954 -6953
  143. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/configuration.json +1 -1
  144. package/dist/esm/utils/AI/askDocQuestion/indexMarkdownFiles.mjs.map +1 -1
  145. package/dist/esm/utils/AI/auditDictionary/index.mjs.map +1 -1
  146. package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
  147. package/dist/esm/utils/AI/auditDictionaryMetadata/index.mjs.map +1 -1
  148. package/dist/esm/utils/AI/auditTag/index.mjs.map +1 -1
  149. package/dist/esm/utils/AI/autocomplete/index.mjs.map +1 -1
  150. package/dist/esm/utils/AI/chat/index.mjs.map +1 -1
  151. package/dist/esm/utils/AI/chat/mcpInProcessTools.mjs.map +1 -1
  152. package/dist/esm/utils/AI/chat/sessionTools.mjs.map +1 -1
  153. package/dist/esm/utils/AI/customQuery/index.mjs.map +1 -1
  154. package/dist/esm/utils/AI/getProjectAIOptions.mjs.map +1 -1
  155. package/dist/esm/utils/AI/translateDictionaryDB.mjs.map +1 -1
  156. package/dist/esm/utils/AI/translateJSON/index.mjs.map +1 -1
  157. package/dist/esm/utils/accessControl.mjs.map +1 -1
  158. package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
  159. package/dist/esm/utils/cors.mjs +2 -13
  160. package/dist/esm/utils/cors.mjs.map +1 -1
  161. package/dist/esm/utils/demoDictionaries.mjs.map +1 -1
  162. package/dist/esm/utils/ensureArrayQueryFilter.mjs.map +1 -1
  163. package/dist/esm/utils/ensureMongoDocumentToObject.mjs.map +1 -1
  164. package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
  165. package/dist/esm/utils/errors/ErrorsClass.mjs.map +1 -1
  166. package/dist/esm/utils/errors/errorCodes.mjs.map +1 -1
  167. package/dist/esm/utils/errors/index.mjs +1 -0
  168. package/dist/esm/utils/filtersAndPagination/getDictionaryFiltersAndPagination.mjs.map +1 -1
  169. package/dist/esm/utils/filtersAndPagination/getDiscussionFiltersAndPagination.mjs.map +1 -1
  170. package/dist/esm/utils/filtersAndPagination/getFiltersAndPaginationFromBody.mjs.map +1 -1
  171. package/dist/esm/utils/filtersAndPagination/getOrganizationFiltersAndPagination.mjs.map +1 -1
  172. package/dist/esm/utils/filtersAndPagination/getProjectFiltersAndPagination.mjs.map +1 -1
  173. package/dist/esm/utils/filtersAndPagination/getTagFiltersAndPagination.mjs.map +1 -1
  174. package/dist/esm/utils/filtersAndPagination/getUserFiltersAndPagination.mjs.map +1 -1
  175. package/dist/esm/utils/getFaviconUrl.mjs.map +1 -1
  176. package/dist/esm/utils/github/connectGithub.mjs.map +1 -1
  177. package/dist/esm/utils/httpStatusCodes.mjs.map +1 -1
  178. package/dist/esm/utils/image/resizeImage.mjs.map +1 -1
  179. package/dist/esm/utils/mapper/dictionary.mjs.map +1 -1
  180. package/dist/esm/utils/mapper/organization.mjs.map +1 -1
  181. package/dist/esm/utils/mapper/project.mjs.map +1 -1
  182. package/dist/esm/utils/mapper/session.mjs.map +1 -1
  183. package/dist/esm/utils/mapper/showcaseProject.mjs.map +1 -1
  184. package/dist/esm/utils/mapper/tag.mjs.map +1 -1
  185. package/dist/esm/utils/mapper/user.mjs.map +1 -1
  186. package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
  187. package/dist/esm/utils/oAuth2.mjs.map +1 -1
  188. package/dist/esm/utils/permissions.mjs.map +1 -1
  189. package/dist/esm/utils/plan.mjs.map +1 -1
  190. package/dist/esm/utils/puppeteer/launchBrowser.mjs.map +1 -1
  191. package/dist/esm/utils/rateLimiter.mjs.map +1 -1
  192. package/dist/esm/utils/redis/connectRedis.mjs.map +1 -1
  193. package/dist/esm/utils/removeObjectKeys.mjs.map +1 -1
  194. package/dist/esm/utils/responseData.mjs.map +1 -1
  195. package/dist/esm/utils/s3/s3Client.mjs.map +1 -1
  196. package/dist/esm/utils/validation/validateDictionary.mjs.map +1 -1
  197. package/dist/esm/utils/validation/validateOrganization.mjs.map +1 -1
  198. package/dist/esm/utils/validation/validateProject.mjs.map +1 -1
  199. package/dist/esm/utils/validation/validateTag.mjs.map +1 -1
  200. package/dist/esm/utils/validation/validateUser.mjs.map +1 -1
  201. package/dist/esm/webhooks/stripe.webhook.mjs.map +1 -1
  202. package/dist/types/controllers/ai.controller.d.ts.map +1 -1
  203. package/dist/types/controllers/bitbucket.controller.d.ts.map +1 -1
  204. package/dist/types/controllers/cliSessionToken.controller.d.ts.map +1 -1
  205. package/dist/types/controllers/demo.controller.d.ts.map +1 -1
  206. package/dist/types/controllers/dictionary.controller.d.ts.map +1 -1
  207. package/dist/types/controllers/environment.controller.d.ts.map +1 -1
  208. package/dist/types/controllers/eventListener.controller.d.ts.map +1 -1
  209. package/dist/types/controllers/github.controller.d.ts.map +1 -1
  210. package/dist/types/controllers/gitlab.controller.d.ts.map +1 -1
  211. package/dist/types/controllers/newsletter.controller.d.ts.map +1 -1
  212. package/dist/types/controllers/oAuth2.controller.d.ts.map +1 -1
  213. package/dist/types/controllers/organization.controller.d.ts.map +1 -1
  214. package/dist/types/controllers/project.controller.d.ts.map +1 -1
  215. package/dist/types/controllers/projectAccessKey.controller.d.ts.map +1 -1
  216. package/dist/types/controllers/projectMemberAccess.controller.d.ts.map +1 -1
  217. package/dist/types/controllers/recursiveAudit.controller.d.ts.map +1 -1
  218. package/dist/types/controllers/reviewer.controller.d.ts.map +1 -1
  219. package/dist/types/controllers/searchDoc.controller.d.ts.map +1 -1
  220. package/dist/types/controllers/showcaseProject.controller.d.ts.map +1 -1
  221. package/dist/types/controllers/stripe.controller.d.ts.map +1 -1
  222. package/dist/types/controllers/tag.controller.d.ts.map +1 -1
  223. package/dist/types/controllers/translation.controller.d.ts.map +1 -1
  224. package/dist/types/controllers/user.controller.d.ts.map +1 -1
  225. package/dist/types/emails/AffiliateActivatedEmail.d.ts +20 -18
  226. package/dist/types/emails/AffiliateActivatedEmail.d.ts.map +1 -1
  227. package/dist/types/emails/AffiliateConversionEmail.d.ts +21 -19
  228. package/dist/types/emails/AffiliateConversionEmail.d.ts.map +1 -1
  229. package/dist/types/emails/AffiliateInvitationEmail.d.ts +20 -18
  230. package/dist/types/emails/AffiliateInvitationEmail.d.ts.map +1 -1
  231. package/dist/types/emails/AffiliateWelcomeEmail.d.ts +20 -18
  232. package/dist/types/emails/AffiliateWelcomeEmail.d.ts.map +1 -1
  233. package/dist/types/emails/InviteUserEmail.d.ts +20 -18
  234. package/dist/types/emails/InviteUserEmail.d.ts.map +1 -1
  235. package/dist/types/emails/MagicLinkEmail.d.ts +20 -18
  236. package/dist/types/emails/MagicLinkEmail.d.ts.map +1 -1
  237. package/dist/types/emails/MissionRequestedClientEmail.d.ts +20 -18
  238. package/dist/types/emails/MissionRequestedClientEmail.d.ts.map +1 -1
  239. package/dist/types/emails/MissionRequestedReviewerEmail.d.ts +20 -18
  240. package/dist/types/emails/MissionRequestedReviewerEmail.d.ts.map +1 -1
  241. package/dist/types/emails/OAuthTokenCreatedEmail.d.ts +20 -18
  242. package/dist/types/emails/OAuthTokenCreatedEmail.d.ts.map +1 -1
  243. package/dist/types/emails/PasswordChangeConfirmation.d.ts +20 -18
  244. package/dist/types/emails/PasswordChangeConfirmation.d.ts.map +1 -1
  245. package/dist/types/emails/ResetUserPassword.d.ts +20 -18
  246. package/dist/types/emails/ResetUserPassword.d.ts.map +1 -1
  247. package/dist/types/emails/ReviewerApplicationEmail.d.ts +20 -18
  248. package/dist/types/emails/ReviewerApplicationEmail.d.ts.map +1 -1
  249. package/dist/types/emails/ReviewerApprovedEmail.d.ts +20 -18
  250. package/dist/types/emails/ReviewerApprovedEmail.d.ts.map +1 -1
  251. package/dist/types/emails/ReviewerContactEmail.d.ts +3 -1
  252. package/dist/types/emails/ReviewerContactEmail.d.ts.map +1 -1
  253. package/dist/types/emails/SubscriptionPaymentCancellation.d.ts +20 -18
  254. package/dist/types/emails/SubscriptionPaymentCancellation.d.ts.map +1 -1
  255. package/dist/types/emails/SubscriptionPaymentError.d.ts +20 -18
  256. package/dist/types/emails/SubscriptionPaymentError.d.ts.map +1 -1
  257. package/dist/types/emails/SubscriptionPaymentSuccess.d.ts +20 -18
  258. package/dist/types/emails/SubscriptionPaymentSuccess.d.ts.map +1 -1
  259. package/dist/types/emails/ValidateUserEmail.d.ts +20 -18
  260. package/dist/types/emails/ValidateUserEmail.d.ts.map +1 -1
  261. package/dist/types/emails/Welcome.d.ts +20 -18
  262. package/dist/types/emails/Welcome.d.ts.map +1 -1
  263. package/dist/types/export.d.ts +1 -1
  264. package/dist/types/logger/index.d.ts +3 -1
  265. package/dist/types/logger/index.d.ts.map +1 -1
  266. package/dist/types/middlewares/oAuth2.middleware.d.ts.map +1 -1
  267. package/dist/types/middlewares/sessionAuth.middleware.d.ts.map +1 -1
  268. package/dist/types/routes/demo.routes.d.ts.map +1 -1
  269. package/dist/types/schemas/account.schema.d.ts +35 -34
  270. package/dist/types/schemas/account.schema.d.ts.map +1 -1
  271. package/dist/types/schemas/affiliate.schema.d.ts +109 -108
  272. package/dist/types/schemas/affiliate.schema.d.ts.map +1 -1
  273. package/dist/types/schemas/affiliateInvitation.schema.d.ts +49 -48
  274. package/dist/types/schemas/affiliateInvitation.schema.d.ts.map +1 -1
  275. package/dist/types/schemas/audit.schema.d.ts.map +1 -1
  276. package/dist/types/schemas/auditJob.schema.d.ts +6 -6
  277. package/dist/types/schemas/auditPage.schema.d.ts +6 -6
  278. package/dist/types/schemas/cliSessionToken.schema.d.ts +14 -13
  279. package/dist/types/schemas/cliSessionToken.schema.d.ts.map +1 -1
  280. package/dist/types/schemas/dictionary.schema.d.ts +60 -59
  281. package/dist/types/schemas/dictionary.schema.d.ts.map +1 -1
  282. package/dist/types/schemas/discussion.schema.d.ts +51 -50
  283. package/dist/types/schemas/discussion.schema.d.ts.map +1 -1
  284. package/dist/types/schemas/oAuth2.schema.d.ts +18 -17
  285. package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
  286. package/dist/types/schemas/organization.schema.d.ts +45 -44
  287. package/dist/types/schemas/organization.schema.d.ts.map +1 -1
  288. package/dist/types/schemas/plans.schema.d.ts +45 -44
  289. package/dist/types/schemas/plans.schema.d.ts.map +1 -1
  290. package/dist/types/schemas/project.schema.d.ts +73 -72
  291. package/dist/types/schemas/project.schema.d.ts.map +1 -1
  292. package/dist/types/schemas/promoCode.schema.d.ts +61 -60
  293. package/dist/types/schemas/promoCode.schema.d.ts.map +1 -1
  294. package/dist/types/schemas/reviewer.schema.d.ts +221 -220
  295. package/dist/types/schemas/reviewer.schema.d.ts.map +1 -1
  296. package/dist/types/schemas/session.schema.d.ts +54 -52
  297. package/dist/types/schemas/session.schema.d.ts.map +1 -1
  298. package/dist/types/schemas/showcaseProject.schema.d.ts +61 -60
  299. package/dist/types/schemas/showcaseProject.schema.d.ts.map +1 -1
  300. package/dist/types/schemas/tag.schema.d.ts +45 -44
  301. package/dist/types/schemas/tag.schema.d.ts.map +1 -1
  302. package/dist/types/schemas/user.schema.d.ts +71 -70
  303. package/dist/types/schemas/user.schema.d.ts.map +1 -1
  304. package/dist/types/services/affiliate.service.d.ts.map +1 -1
  305. package/dist/types/services/audit/analysis/analyzeBundleContent.d.ts.map +1 -1
  306. package/dist/types/services/audit/analysis/analyzeLinguisticStructure.d.ts.map +1 -1
  307. package/dist/types/services/audit/analysis/analyzeRobots.d.ts.map +1 -1
  308. package/dist/types/services/audit/analysis/analyzeSitemap.d.ts.map +1 -1
  309. package/dist/types/services/audit/analysis/calculateScore.d.ts.map +1 -1
  310. package/dist/types/services/audit/checkers/bundleChecker.d.ts.map +1 -1
  311. package/dist/types/services/audit/recursiveAudit.service.d.ts +5 -4
  312. package/dist/types/services/audit/recursiveAudit.service.d.ts.map +1 -1
  313. package/dist/types/services/audit/types.d.ts.map +1 -1
  314. package/dist/types/services/bitbucket.service.d.ts.map +1 -1
  315. package/dist/types/services/cliSessionToken.service.d.ts.map +1 -1
  316. package/dist/types/services/dictionary.service.d.ts.map +1 -1
  317. package/dist/types/services/email.service.d.ts.map +1 -1
  318. package/dist/types/services/github.service.d.ts.map +1 -1
  319. package/dist/types/services/gitlab.service.d.ts.map +1 -1
  320. package/dist/types/services/oAuth2.service.d.ts.map +1 -1
  321. package/dist/types/services/organization.service.d.ts.map +1 -1
  322. package/dist/types/services/project/projectScreenshot.service.d.ts.map +1 -1
  323. package/dist/types/services/project.service.d.ts.map +1 -1
  324. package/dist/types/services/promoCode.service.d.ts.map +1 -1
  325. package/dist/types/services/reviewer/pictureUpload.service.d.ts.map +1 -1
  326. package/dist/types/services/reviewer.service.d.ts.map +1 -1
  327. package/dist/types/services/reviewerMessage.service.d.ts.map +1 -1
  328. package/dist/types/services/reviewerMission.service.d.ts.map +1 -1
  329. package/dist/types/services/session.service.d.ts.map +1 -1
  330. package/dist/types/services/showcase/showcaseProject.service.d.ts.map +1 -1
  331. package/dist/types/services/showcase/showcaseScan.service.d.ts.map +1 -1
  332. package/dist/types/services/showcase/showcaseUploadScreenshot.service.d.ts.map +1 -1
  333. package/dist/types/services/showcase/showcaseVerifyBundle.service.d.ts.map +1 -1
  334. package/dist/types/services/showcase/showcaseVerifyGithub.service.d.ts.map +1 -1
  335. package/dist/types/services/subscription.service.d.ts.map +1 -1
  336. package/dist/types/services/tag.service.d.ts.map +1 -1
  337. package/dist/types/services/translationQueue.service.d.ts +2 -1
  338. package/dist/types/services/translationQueue.service.d.ts.map +1 -1
  339. package/dist/types/services/translationWorker.service.d.ts.map +1 -1
  340. package/dist/types/services/user/avatarUpload.service.d.ts.map +1 -1
  341. package/dist/types/services/user.service.d.ts.map +1 -1
  342. package/dist/types/services/webhook.service.d.ts.map +1 -1
  343. package/dist/types/types/Routes.d.ts.map +1 -1
  344. package/dist/types/types/account.types.d.ts.map +1 -1
  345. package/dist/types/types/affiliate.types.d.ts.map +1 -1
  346. package/dist/types/types/affiliateInvitation.types.d.ts.map +1 -1
  347. package/dist/types/types/dictionary.types.d.ts.map +1 -1
  348. package/dist/types/types/discussion.types.d.ts.map +1 -1
  349. package/dist/types/types/oAuth2.types.d.ts.map +1 -1
  350. package/dist/types/types/organization.types.d.ts.map +1 -1
  351. package/dist/types/types/plan.types.d.ts.map +1 -1
  352. package/dist/types/types/project.types.d.ts.map +1 -1
  353. package/dist/types/types/promoCode.types.d.ts.map +1 -1
  354. package/dist/types/types/reviewer.types.d.ts.map +1 -1
  355. package/dist/types/types/session.types.d.ts.map +1 -1
  356. package/dist/types/types/showcaseProject.types.d.ts.map +1 -1
  357. package/dist/types/types/tag.types.d.ts.map +1 -1
  358. package/dist/types/types/user.types.d.ts.map +1 -1
  359. package/dist/types/utils/AI/askDocQuestion/askDocQuestion.d.ts.map +1 -1
  360. package/dist/types/utils/AI/askDocQuestion/indexMarkdownFiles.d.ts.map +1 -1
  361. package/dist/types/utils/AI/auditDictionary/index.d.ts.map +1 -1
  362. package/dist/types/utils/AI/auditDictionaryField/index.d.ts.map +1 -1
  363. package/dist/types/utils/AI/auditDictionaryMetadata/index.d.ts.map +1 -1
  364. package/dist/types/utils/AI/auditTag/index.d.ts.map +1 -1
  365. package/dist/types/utils/AI/autocomplete/index.d.ts.map +1 -1
  366. package/dist/types/utils/AI/chat/index.d.ts.map +1 -1
  367. package/dist/types/utils/AI/chat/mcpInProcessTools.d.ts.map +1 -1
  368. package/dist/types/utils/AI/chat/sessionTools.d.ts +25 -24
  369. package/dist/types/utils/AI/chat/sessionTools.d.ts.map +1 -1
  370. package/dist/types/utils/AI/customQuery/index.d.ts.map +1 -1
  371. package/dist/types/utils/AI/translateDictionaryDB.d.ts.map +1 -1
  372. package/dist/types/utils/AI/translateJSON/index.d.ts.map +1 -1
  373. package/dist/types/utils/auth/getAuth.d.ts.map +1 -1
  374. package/dist/types/utils/cors.d.ts +1 -7
  375. package/dist/types/utils/cors.d.ts.map +1 -1
  376. package/dist/types/utils/demoDictionaries.d.ts.map +1 -1
  377. package/dist/types/utils/ensureArrayQueryFilter.d.ts.map +1 -1
  378. package/dist/types/utils/errors/ErrorHandler.d.ts +8 -6
  379. package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
  380. package/dist/types/utils/errors/ErrorsClass.d.ts.map +1 -1
  381. package/dist/types/utils/errors/errorCodes.d.ts.map +1 -1
  382. package/dist/types/utils/filtersAndPagination/getDictionaryFiltersAndPagination.d.ts.map +1 -1
  383. package/dist/types/utils/filtersAndPagination/getDiscussionFiltersAndPagination.d.ts.map +1 -1
  384. package/dist/types/utils/filtersAndPagination/getFiltersAndPaginationFromBody.d.ts.map +1 -1
  385. package/dist/types/utils/filtersAndPagination/getOrganizationFiltersAndPagination.d.ts.map +1 -1
  386. package/dist/types/utils/filtersAndPagination/getProjectFiltersAndPagination.d.ts.map +1 -1
  387. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +7 -6
  388. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts.map +1 -1
  389. package/dist/types/utils/filtersAndPagination/getUserFiltersAndPagination.d.ts.map +1 -1
  390. package/dist/types/utils/getFaviconUrl.d.ts.map +1 -1
  391. package/dist/types/utils/github/connectGithub.d.ts.map +1 -1
  392. package/dist/types/utils/httpStatusCodes.d.ts.map +1 -1
  393. package/dist/types/utils/image/resizeImage.d.ts.map +1 -1
  394. package/dist/types/utils/mapper/dictionary.d.ts.map +1 -1
  395. package/dist/types/utils/mapper/project.d.ts.map +1 -1
  396. package/dist/types/utils/mapper/session.d.ts.map +1 -1
  397. package/dist/types/utils/mapper/showcaseProject.d.ts.map +1 -1
  398. package/dist/types/utils/mapper/tag.d.ts.map +1 -1
  399. package/dist/types/utils/mergeFunctionTypes.d.ts.map +1 -1
  400. package/dist/types/utils/mongoDB/connectDB.d.ts.map +1 -1
  401. package/dist/types/utils/mongoDB/types.d.ts.map +1 -1
  402. package/dist/types/utils/oAuth2.d.ts.map +1 -1
  403. package/dist/types/utils/permissions.d.ts +2 -1
  404. package/dist/types/utils/permissions.d.ts.map +1 -1
  405. package/dist/types/utils/plan.d.ts.map +1 -1
  406. package/dist/types/utils/rateLimiter.d.ts.map +1 -1
  407. package/dist/types/utils/redis/connectRedis.d.ts.map +1 -1
  408. package/dist/types/utils/responseData.d.ts.map +1 -1
  409. package/dist/types/utils/s3/s3Client.d.ts.map +1 -1
  410. package/dist/types/utils/validation/validateDictionary.d.ts.map +1 -1
  411. package/dist/types/utils/validation/validateOrganization.d.ts.map +1 -1
  412. package/dist/types/utils/validation/validateProject.d.ts.map +1 -1
  413. package/dist/types/utils/validation/validateTag.d.ts.map +1 -1
  414. package/dist/types/utils/validation/validateUser.d.ts.map +1 -1
  415. package/package.json +17 -17
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.service.mjs","names":[],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { PromoCodeModel } from '@schemas/promoCode.schema';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const isCanceled = status === 'canceled';\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId: isCanceled ? undefined : priceId,\n customerId,\n subscriptionId: isCanceled ? undefined : subscriptionId,\n type: isCanceled ? 'FREE' : planInfo.type,\n period: isCanceled ? undefined : planInfo.period,\n status: isCanceled ? 'active' : status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | undefined, // Changed to optional\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n organizationId,\n });\n }\n\n // If there is no plan, we consider it already \"canceled\" or free.\n // We can return a default free plan or just null.\n if (!organization.plan) {\n return {\n type: 'FREE',\n status: 'active',\n } as Plan;\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'active',\n type: 'FREE',\n period: undefined,\n subscriptionId: undefined,\n priceId: undefined,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}${subscriptionId ? ` (Subscription ID: ${subscriptionId})` : ''}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const isCanceled = status === 'canceled';\n\n const updatedOrganization = await updatePlan(\n organization,\n isCanceled\n ? {\n status: 'active',\n type: 'FREE',\n period: undefined,\n subscriptionId: undefined,\n priceId: undefined,\n }\n : {\n status,\n subscriptionId,\n }\n );\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName:\n updatedOrganization?.plan?.type ?? organization.plan?.type ?? 'Unknown',\n date: new Date().toLocaleDateString(),\n link: `${process.env.APP_URL}/dashboard`,\n billingLink: `${process.env.APP_URL}/organization`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<{ couponId: string | null; affiliateId?: string }> => {\n if (!promoCode) return { couponId: null };\n\n try {\n // 1. Check our DB first\n const dbCode = await PromoCodeModel.findOne({\n code: promoCode.toUpperCase(),\n active: true,\n });\n if (dbCode) {\n return {\n couponId: dbCode.stripeCouponId,\n affiliateId: dbCode.affiliateId\n ? String(dbCode.affiliateId)\n : undefined,\n };\n }\n\n // 2. Fall back to direct Stripe lookup (backwards compat)\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const coupons = await stripe.coupons.list();\n const match = coupons.data.find((c) => c.name === promoCode);\n return { couponId: match ? match.id : null };\n } catch (error) {\n logger.error('Error retrieving coupon:', error);\n return { couponId: null };\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n planType: 'premium' | 'enterprise' | 'one_time' | 'unknown';\n period: 'monthly' | 'yearly' | 'one_time' | 'unknown';\n }\n>;\n\nexport const getPricing = async (\n priceIds?: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n const idsToFetch =\n priceIds && priceIds.length > 0\n ? priceIds\n : ([\n process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID,\n process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID,\n process.env.STRIPE_ENTERPRISE_YEARLY_PRICE_ID,\n process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID,\n process.env.STRIPE_ONE_TIME_PAYMENT_PRICE_ID,\n ].filter(Boolean) as string[]);\n\n // Fetch all price objects, skipping any that fail (e.g. unset / invalid\n // env IDs). One bad ID should not break pricing for the entire page.\n const priceResults = await Promise.allSettled(\n idsToFetch.map((priceId) => stripe.prices.retrieve(priceId))\n );\n\n const prices = priceResults\n .map((result, index) => {\n if (result.status === 'fulfilled') return result.value;\n logger.warn(\n `Skipping price ${idsToFetch[index]} — retrieval failed: ${\n (result.reason as Error)?.message ?? 'unknown error'\n }`\n );\n return null;\n })\n .filter((price): price is NonNullable<typeof price> => price !== null);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price?.unit_amount ?? 0),\n 0\n );\n\n // Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const { couponId } = await getCouponId(promoCode);\n if (couponId) {\n try {\n const coupon = await stripe.coupons.retrieve(couponId);\n if (coupon.amount_off) {\n discountAmount = coupon.amount_off;\n discountType = 'amount';\n } else if (coupon.percent_off) {\n discountAmount = coupon.percent_off;\n discountType = 'percentage';\n }\n } catch (err) {\n logger.warn(`Failed to retrieve coupon ${couponId}: ${err}`);\n }\n }\n }\n\n // Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price?.id || !price?.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price?.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n let planType: 'premium' | 'enterprise' | 'one_time' | 'unknown' =\n 'unknown';\n let period: 'monthly' | 'yearly' | 'one_time' | 'unknown' = 'unknown';\n\n if (price.id === process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID) {\n planType = 'premium';\n period = 'yearly';\n } else if (price.id === process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID) {\n planType = 'premium';\n period = 'monthly';\n } else if (price.id === process.env.STRIPE_ENTERPRISE_YEARLY_PRICE_ID) {\n planType = 'enterprise';\n period = 'yearly';\n } else if (price.id === process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID) {\n planType = 'enterprise';\n period = 'monthly';\n } else if (price.id === process.env.STRIPE_ONE_TIME_PAYMENT_PRICE_ID) {\n planType = 'one_time';\n period = 'one_time';\n }\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n planType,\n period,\n };\n }\n\n return results;\n } catch (error) {\n logger.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"subscription.service.mjs","names":[],"sources":["../../../src/services/subscription.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { PromoCodeModel } from '@schemas/promoCode.schema';\nimport { GenericError } from '@utils/errors';\nimport { retrievePlanInformation } from '@utils/plan';\nimport Stripe from 'stripe';\nimport type { Organization } from '@/types/organization.types';\nimport type { Plan } from '@/types/plan.types';\nimport { sendEmail } from './email.service';\nimport { getOrganizationById, updatePlan } from './organization.service';\nimport { getUserById } from './user.service';\n\nexport const addOrUpdateSubscription = async (\n subscriptionId: string,\n priceId: string,\n customerId: string,\n userId: string,\n organization: Organization,\n status: Plan['status']\n): Promise<Plan | null> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n });\n }\n\n if (String(user.customerId) !== customerId) {\n (user.customerId as unknown as string) = customerId;\n await user.save();\n }\n\n const planInfo = retrievePlanInformation(priceId);\n\n const subscriptions = await stripe.subscriptions.list({\n customer: customerId,\n status: 'active',\n });\n\n if (subscriptions.data.length >= 1) {\n // Active subscription exists; update it to the new plan\n const otherSubscriptionArray = subscriptions.data.filter(\n (subscription) => subscription.id !== subscriptionId\n );\n\n for (const subscription of otherSubscriptionArray) {\n await stripe.subscriptions.cancel(subscription.id);\n }\n }\n\n const isCanceled = status === 'canceled';\n\n const updatedOrganization = await updatePlan(organization, {\n creatorId: user.id,\n priceId: isCanceled ? undefined : priceId,\n customerId,\n subscriptionId: isCanceled ? undefined : subscriptionId,\n type: isCanceled ? 'FREE' : planInfo.type,\n period: isCanceled ? undefined : planInfo.period,\n status: isCanceled ? 'active' : status,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Plan updated for organization ${organization.id} - ${planInfo.type} - ${planInfo.period}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const cancelSubscription = async (\n subscriptionId: string | undefined, // Changed to optional\n organizationId: Organization['id'] | string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n organizationId,\n });\n }\n\n // If there is no plan, we consider it already \"canceled\" or free.\n // We can return a default free plan or just null.\n if (!organization.plan) {\n return {\n type: 'FREE',\n status: 'active',\n } as Plan;\n }\n\n const updatedOrganization = await updatePlan(organization, {\n status: 'active',\n type: 'FREE',\n period: undefined,\n subscriptionId: undefined,\n priceId: undefined,\n });\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n logger.info(\n `Cancelled plan for organization ${updatedOrganization.id} - ${updatedOrganization.plan?.type} - ${updatedOrganization.plan?.period}${subscriptionId ? ` (Subscription ID: ${subscriptionId})` : ''}`\n );\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const changeSubscriptionStatus = async (\n subscriptionId: string,\n status: Plan['status'],\n userId: string,\n organizationId: string\n): Promise<Plan | null> => {\n const organization = await getOrganizationById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n if (!organization.plan) {\n throw new GenericError('ORGANIZATION_PLAN_NOT_FOUND', {\n userId,\n subscriptionId,\n organizationId: organization.id,\n });\n }\n\n const isCanceled = status === 'canceled';\n\n const updatedOrganization = await updatePlan(\n organization,\n isCanceled\n ? {\n status: 'active',\n type: 'FREE',\n period: undefined,\n subscriptionId: undefined,\n priceId: undefined,\n }\n : {\n status,\n subscriptionId,\n }\n );\n\n if (!updatedOrganization) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const user = await getUserById(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', {\n userId,\n subscriptionId,\n });\n }\n\n logger.info(\n `Updated plan status for organization ${organization.id} - Status: ${status}`\n );\n\n const emailData = {\n to: user.email,\n username: user.name,\n email: user.email,\n planName:\n updatedOrganization?.plan?.type ?? organization.plan?.type ?? 'Unknown',\n date: new Date().toLocaleDateString(),\n link: `${process.env.APP_URL}/dashboard`,\n billingLink: `${process.env.APP_URL}/organization`,\n };\n\n switch (status) {\n case 'active':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentSuccess',\n organizationName: organization.name,\n subscriptionStartDate: emailData.date,\n manageSubscriptionLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n case 'canceled':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentCancellation',\n organizationName: organization.name,\n cancellationDate: emailData.date,\n reactivateLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n case 'incomplete':\n await sendEmail({\n ...emailData,\n type: 'subscriptionPaymentError',\n organizationName: organization.name,\n errorDate: emailData.date,\n retryPaymentLink: emailData.link,\n billingLink: emailData.billingLink,\n });\n break;\n default:\n logger.warn(`Unhandled subscription status: ${status}`);\n }\n\n return updatedOrganization.plan ?? null;\n};\n\nexport const getCouponId = async (\n promoCode: string\n): Promise<{ couponId: string | null; affiliateId?: string }> => {\n if (!promoCode) return { couponId: null };\n\n try {\n // 1. Check our DB first\n const dbCode = await PromoCodeModel.findOne({\n code: promoCode.toUpperCase(),\n active: true,\n });\n if (dbCode) {\n return {\n couponId: dbCode.stripeCouponId,\n affiliateId: dbCode.affiliateId\n ? String(dbCode.affiliateId)\n : undefined,\n };\n }\n\n // 2. Fall back to direct Stripe lookup (backwards compat)\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n const coupons = await stripe.coupons.list();\n const match = coupons.data.find((c) => c.name === promoCode);\n return { couponId: match ? match.id : null };\n } catch (error) {\n logger.error('Error retrieving coupon:', error);\n return { couponId: null };\n }\n};\n\nexport type PricingResult = Record<\n string,\n {\n originalTotal: number;\n discountApplied: number;\n discountType: 'amount' | 'percentage' | null;\n finalTotal: number;\n currency: string;\n planType: 'premium' | 'enterprise' | 'one_time' | 'unknown';\n period: 'monthly' | 'yearly' | 'one_time' | 'unknown';\n }\n>;\n\nexport const getPricing = async (\n priceIds?: string[],\n promoCode?: string\n): Promise<PricingResult> => {\n const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);\n\n try {\n const idsToFetch =\n priceIds && priceIds.length > 0\n ? priceIds\n : ([\n process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID,\n process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID,\n process.env.STRIPE_ENTERPRISE_YEARLY_PRICE_ID,\n process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID,\n process.env.STRIPE_ONE_TIME_PAYMENT_PRICE_ID,\n ].filter(Boolean) as string[]);\n\n // Fetch all price objects, skipping any that fail (e.g. unset / invalid\n // env IDs). One bad ID should not break pricing for the entire page.\n const priceResults = await Promise.allSettled(\n idsToFetch.map((priceId) => stripe.prices.retrieve(priceId))\n );\n\n const prices = priceResults\n .map((result, index) => {\n if (result.status === 'fulfilled') return result.value;\n logger.warn(\n `Skipping price ${idsToFetch[index]} — retrieval failed: ${\n (result.reason as Error)?.message ?? 'unknown error'\n }`\n );\n return null;\n })\n .filter((price): price is NonNullable<typeof price> => price !== null);\n\n // Calculate the total amount before discount (to help with proportional distribution if needed)\n const totalAmount = prices.reduce(\n (sum, price) => sum + (price?.unit_amount ?? 0),\n 0\n );\n\n // Retrieve the discount (if promo code is provided)\n let discountAmount = 0;\n let discountType: 'amount' | 'percentage' | null = null;\n\n if (promoCode) {\n const { couponId } = await getCouponId(promoCode);\n if (couponId) {\n try {\n const coupon = await stripe.coupons.retrieve(couponId);\n if (coupon.amount_off) {\n discountAmount = coupon.amount_off;\n discountType = 'amount';\n } else if (coupon.percent_off) {\n discountAmount = coupon.percent_off;\n discountType = 'percentage';\n }\n } catch (err) {\n logger.warn(`Failed to retrieve coupon ${couponId}: ${err}`);\n }\n }\n }\n\n // Build the result for each priceId\n const results: PricingResult = {};\n\n for (const price of prices) {\n if (!price?.id || !price?.unit_amount) {\n continue; // Skip any invalid price\n }\n\n const originalTotal = price?.unit_amount;\n let appliedDiscount = 0;\n let finalTotal = originalTotal;\n\n // Apply discount based on the discount type\n if (discountType === 'percentage' && discountAmount > 0) {\n // percentage-based discount\n appliedDiscount = (originalTotal * discountAmount) / 100;\n finalTotal = originalTotal - appliedDiscount;\n } else if (\n discountType === 'amount' &&\n totalAmount > 0 &&\n discountAmount > 0\n ) {\n // fixed amount discount - distribute proportionally\n const proportion = originalTotal / totalAmount;\n appliedDiscount = discountAmount * proportion;\n finalTotal = originalTotal - appliedDiscount;\n }\n\n // Prevent final total from going negative due to rounding\n finalTotal = Math.max(finalTotal, 0);\n\n let planType: 'premium' | 'enterprise' | 'one_time' | 'unknown' =\n 'unknown';\n let period: 'monthly' | 'yearly' | 'one_time' | 'unknown' = 'unknown';\n\n if (price.id === process.env.STRIPE_PREMIUM_YEARLY_PRICE_ID) {\n planType = 'premium';\n period = 'yearly';\n } else if (price.id === process.env.STRIPE_PREMIUM_MONTHLY_PRICE_ID) {\n planType = 'premium';\n period = 'monthly';\n } else if (price.id === process.env.STRIPE_ENTERPRISE_YEARLY_PRICE_ID) {\n planType = 'enterprise';\n period = 'yearly';\n } else if (price.id === process.env.STRIPE_ENTERPRISE_MONTHLY_PRICE_ID) {\n planType = 'enterprise';\n period = 'monthly';\n } else if (price.id === process.env.STRIPE_ONE_TIME_PAYMENT_PRICE_ID) {\n planType = 'one_time';\n period = 'one_time';\n }\n\n results[price.id] = {\n originalTotal: originalTotal,\n discountApplied: appliedDiscount,\n discountType,\n finalTotal: finalTotal,\n currency: price.currency,\n planType,\n period,\n };\n }\n\n return results;\n } catch (error) {\n logger.error('Error calculating pricing per priceId:', error);\n throw new Error('Failed to calculate pricing breakdown.');\n }\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tag.service.mjs","names":[],"sources":["../../../src/services/tag.service.ts"],"sourcesContent":["import { TagModel } from '@schemas/tag.schema';\nimport { GenericError } from '@utils/errors';\nimport type { TagFilters } from '@utils/filtersAndPagination/getTagFiltersAndPagination';\nimport { type TagFields, validateTag } from '@utils/validation/validateTag';\nimport type { Types } from 'mongoose';\nimport type { Organization } from '@/types/organization.types';\nimport type { Tag, TagData, TagDocument } from '@/types/tag.types';\n\n/**\n * Finds tags based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of tags matching the filters.\n */\nexport const findTags = async (\n filters: TagFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>\n): Promise<TagDocument[]> => {\n let query = TagModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Finds a tag by its ID.\n * @param tagId - The ID of the tag to find.\n * @returns The tag matching the ID.\n */\nexport const getTagById = async (\n tagId: string | Types.ObjectId\n): Promise<TagDocument> => {\n const tag = await TagModel.findById(tagId);\n\n if (!tag) {\n throw new GenericError('TAG_NOT_FOUND', { tagId });\n }\n\n return tag;\n};\n\nexport const getTagsByKeys = async (\n keys: string[],\n organizationId: string | Organization['id']\n): Promise<TagDocument[]> => {\n const tags = await TagModel.find({ key: { $in: keys }, organizationId });\n\n return tags;\n};\n\n/**\n * Counts the total number of tags that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of tags.\n */\nexport const countTags = async (filters: TagFilters): Promise<number> => {\n const result = await TagModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('TAG_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new tag in the database.\n * @param tag - The tag data to create.\n * @returns The created tag.\n */\nexport const createTag = async (tag: TagData): Promise<TagDocument> => {\n const errors = await validateTag(tag, ['key']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('TAG_INVALID_FIELDS', { errors });\n }\n\n return await TagModel.create(tag);\n};\n\n/**\n * Updates an existing tag in the database by its ID.\n * @param tagId - The ID of the tag to update.\n * @param tag - The updated tag data.\n * @returns The updated tag.\n */\nexport const updateTagById = async (\n tagId: string | Types.ObjectId,\n tag: Partial<Tag>\n): Promise<TagDocument> => {\n const updatedKeys = Object.keys(tag) as TagFields;\n\n const errors = validateTag(tag, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('TAG_INVALID_FIELDS', {\n tagId,\n errors,\n });\n }\n\n const result = await TagModel.updateOne({ _id: tagId }, tag);\n\n if (result.matchedCount === 0) {\n throw new GenericError('TAG_UPDATE_FAILED', { tagId });\n }\n\n return await getTagById(tagId);\n};\n\n/**\n * Deletes a tag from the database by its ID.\n * @param tagId - The ID of the tag to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteTagById = async (\n tagId: string | Types.ObjectId\n): Promise<TagDocument> => {\n const tag = await TagModel.findByIdAndDelete(tagId);\n\n if (!tag) {\n throw new GenericError('TAG_NOT_FOUND', { tagId });\n }\n\n return tag;\n};\n"],"mappings":";;;;;;;;;;;;AAeA,MAAa,WAAW,OACtB,SACA,OAAO,GACP,QAAQ,KACR,gBAC2B;CAC3B,IAAI,QAAQ,SAAS,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK;CAEzD,IAAI,eAAe,OAAO,KAAK,WAAW,CAAC,CAAC,SAAS,GACnD,QAAQ,MAAM,KAAK,WAAW;CAGhC,OAAO,MAAM;AACf;;;;;;AAOA,MAAa,aAAa,OACxB,UACyB;CACzB,MAAM,MAAM,MAAM,SAAS,SAAS,KAAK;CAEzC,IAAI,CAAC,KACH,MAAM,IAAI,aAAa,iBAAiB,EAAE,MAAM,CAAC;CAGnD,OAAO;AACT;AAEA,MAAa,gBAAgB,OAC3B,MACA,mBAC2B;CAG3B,OAAO,MAFY,SAAS,KAAK;EAAE,KAAK,EAAE,KAAK,KAAK;EAAG;CAAe,CAAC;AAGzE;;;;;;AAOA,MAAa,YAAY,OAAO,YAAyC;CACvE,MAAM,SAAS,MAAM,SAAS,eAAe,OAAO;CAEpD,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,oBAAoB,EAAE,QAAQ,CAAC;CAGxD,OAAO;AACT;;;;;;AAOA,MAAa,YAAY,OAAO,QAAuC;CACrE,MAAM,SAAS,MAAM,YAAY,KAAK,CAAC,KAAK,CAAC;CAE7C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,sBAAsB,EAAE,OAAO,CAAC;CAGzD,OAAO,MAAM,SAAS,OAAO,GAAG;AAClC;;;;;;;AAQA,MAAa,gBAAgB,OAC3B,OACA,QACyB;CAGzB,MAAM,SAAS,YAAY,KAFP,OAAO,KAAK,GAEU,CAAC;CAE3C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,sBAAsB;EAC3C;EACA;CACF,CAAC;CAKH,KAAI,MAFiB,SAAS,UAAU,EAAE,KAAK,MAAM,GAAG,GAAG,EAEjD,CAAC,iBAAiB,GAC1B,MAAM,IAAI,aAAa,qBAAqB,EAAE,MAAM,CAAC;CAGvD,OAAO,MAAM,WAAW,KAAK;AAC/B;;;;;;AAOA,MAAa,gBAAgB,OAC3B,UACyB;CACzB,MAAM,MAAM,MAAM,SAAS,kBAAkB,KAAK;CAElD,IAAI,CAAC,KACH,MAAM,IAAI,aAAa,iBAAiB,EAAE,MAAM,CAAC;CAGnD,OAAO;AACT"}
1
+ {"version":3,"file":"tag.service.mjs","names":[],"sources":["../../../src/services/tag.service.ts"],"sourcesContent":["import { TagModel } from '@schemas/tag.schema';\nimport { GenericError } from '@utils/errors';\nimport type { TagFilters } from '@utils/filtersAndPagination/getTagFiltersAndPagination';\nimport { type TagFields, validateTag } from '@utils/validation/validateTag';\nimport type { Types } from 'mongoose';\nimport type { Organization } from '@/types/organization.types';\nimport type { Tag, TagData, TagDocument } from '@/types/tag.types';\n\n/**\n * Finds tags based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @returns List of tags matching the filters.\n */\nexport const findTags = async (\n filters: TagFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>\n): Promise<TagDocument[]> => {\n let query = TagModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Finds a tag by its ID.\n * @param tagId - The ID of the tag to find.\n * @returns The tag matching the ID.\n */\nexport const getTagById = async (\n tagId: string | Types.ObjectId\n): Promise<TagDocument> => {\n const tag = await TagModel.findById(tagId);\n\n if (!tag) {\n throw new GenericError('TAG_NOT_FOUND', { tagId });\n }\n\n return tag;\n};\n\nexport const getTagsByKeys = async (\n keys: string[],\n organizationId: string | Organization['id']\n): Promise<TagDocument[]> => {\n const tags = await TagModel.find({ key: { $in: keys }, organizationId });\n\n return tags;\n};\n\n/**\n * Counts the total number of tags that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of tags.\n */\nexport const countTags = async (filters: TagFilters): Promise<number> => {\n const result = await TagModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('TAG_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new tag in the database.\n * @param tag - The tag data to create.\n * @returns The created tag.\n */\nexport const createTag = async (tag: TagData): Promise<TagDocument> => {\n const errors = await validateTag(tag, ['key']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('TAG_INVALID_FIELDS', { errors });\n }\n\n return await TagModel.create(tag);\n};\n\n/**\n * Updates an existing tag in the database by its ID.\n * @param tagId - The ID of the tag to update.\n * @param tag - The updated tag data.\n * @returns The updated tag.\n */\nexport const updateTagById = async (\n tagId: string | Types.ObjectId,\n tag: Partial<Tag>\n): Promise<TagDocument> => {\n const updatedKeys = Object.keys(tag) as TagFields;\n\n const errors = validateTag(tag, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('TAG_INVALID_FIELDS', {\n tagId,\n errors,\n });\n }\n\n const result = await TagModel.updateOne({ _id: tagId }, tag);\n\n if (result.matchedCount === 0) {\n throw new GenericError('TAG_UPDATE_FAILED', { tagId });\n }\n\n return await getTagById(tagId);\n};\n\n/**\n * Deletes a tag from the database by its ID.\n * @param tagId - The ID of the tag to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteTagById = async (\n tagId: string | Types.ObjectId\n): Promise<TagDocument> => {\n const tag = await TagModel.findByIdAndDelete(tagId);\n\n if (!tag) {\n throw new GenericError('TAG_NOT_FOUND', { tagId });\n }\n\n return tag;\n};\n"],"mappings":";;;;;;;;;;;;AAeA,MAAa,WAAW,OACtB,SACA,OAAO,GACP,QAAQ,KACR,gBAC2B;CAC3B,IAAI,QAAQ,SAAS,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,MAAM,MAAM;AAE1D,KAAI,eAAe,OAAO,KAAK,YAAY,CAAC,SAAS,EACnD,SAAQ,MAAM,KAAK,YAAY;AAGjC,QAAO,MAAM;;;;;;;AAQf,MAAa,aAAa,OACxB,UACyB;CACzB,MAAM,MAAM,MAAM,SAAS,SAAS,MAAM;AAE1C,KAAI,CAAC,IACH,OAAM,IAAI,aAAa,iBAAiB,EAAE,OAAO,CAAC;AAGpD,QAAO;;AAGT,MAAa,gBAAgB,OAC3B,MACA,mBAC2B;AAG3B,QAAO,MAFY,SAAS,KAAK;EAAE,KAAK,EAAE,KAAK,MAAM;EAAE;EAAgB,CAAC;;;;;;;AAU1E,MAAa,YAAY,OAAO,YAAyC;CACvE,MAAM,SAAS,MAAM,SAAS,eAAe,QAAQ;AAErD,KAAI,OAAO,WAAW,YACpB,OAAM,IAAI,aAAa,oBAAoB,EAAE,SAAS,CAAC;AAGzD,QAAO;;;;;;;AAQT,MAAa,YAAY,OAAO,QAAuC;CACrE,MAAM,SAAS,MAAM,YAAY,KAAK,CAAC,MAAM,CAAC;AAE9C,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,sBAAsB,EAAE,QAAQ,CAAC;AAG1D,QAAO,MAAM,SAAS,OAAO,IAAI;;;;;;;;AASnC,MAAa,gBAAgB,OAC3B,OACA,QACyB;CAGzB,MAAM,SAAS,YAAY,KAFP,OAAO,KAAK,IAEW,CAAC;AAE5C,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,sBAAsB;EAC3C;EACA;EACD,CAAC;AAKJ,MAAI,MAFiB,SAAS,UAAU,EAAE,KAAK,OAAO,EAAE,IAAI,EAEjD,iBAAiB,EAC1B,OAAM,IAAI,aAAa,qBAAqB,EAAE,OAAO,CAAC;AAGxD,QAAO,MAAM,WAAW,MAAM;;;;;;;AAQhC,MAAa,gBAAgB,OAC3B,UACyB;CACzB,MAAM,MAAM,MAAM,SAAS,kBAAkB,MAAM;AAEnD,KAAI,CAAC,IACH,OAAM,IAAI,aAAa,iBAAiB,EAAE,OAAO,CAAC;AAGpD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"translationQueue.service.mjs","names":[],"sources":["../../../src/services/translationQueue.service.ts"],"sourcesContent":["import type { ConnectionOptions } from 'node:tls';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { Queue, QueueEvents } from 'bullmq';\n\nexport const translationQueueName = `translation-queue-${process.env.NODE_ENV}`;\n\nlet translationQueueInstance: Queue | null = null;\nlet translationQueueEventsInstance: QueueEvents | null = null;\n\nexport const getTranslationQueue = () => {\n if (translationQueueInstance) {\n return translationQueueInstance;\n }\n\n const connection = getRedisClient();\n\n translationQueueInstance = new Queue(translationQueueName, {\n connection: connection as unknown as ConnectionOptions,\n });\n\n return translationQueueInstance;\n};\n\nexport const getTranslationQueueEvents = () => {\n if (translationQueueEventsInstance) {\n return translationQueueEventsInstance;\n }\n\n const connection = getRedisClient();\n\n translationQueueEventsInstance = new QueueEvents(translationQueueName, {\n connection: connection as unknown as ConnectionOptions,\n });\n\n return translationQueueEventsInstance;\n};\n\nexport const translationPauseKey = (jobId: string) =>\n `translate:pause:${jobId}`;\nexport const translationCancelKey = (jobId: string) =>\n `translate:cancel:${jobId}`;\n\nexport const isTranslationJobPaused = async (\n jobId: string\n): Promise<boolean> => {\n const redis = getRedisClient();\n return !!(await redis.get(translationPauseKey(jobId)));\n};\n\nexport const isTranslationJobCancelled = async (\n jobId: string\n): Promise<boolean> => {\n const redis = getRedisClient();\n return !!(await redis.get(translationCancelKey(jobId)));\n};\n\nexport const addTranslationJob = async (data: {\n dictionaryTargets: { dictionaryId: string; locales: Locale[] }[];\n projectId: string;\n userId: string;\n mode?: 'complete' | 'review';\n}) => {\n try {\n const queue = getTranslationQueue();\n const job = await queue.add('translate-dictionaries', data);\n logger.info(`Translation job added: ${job.id}`);\n return job;\n } catch (error) {\n logger.error('Error adding translation job:', error);\n throw error;\n }\n};\n"],"mappings":";;;;;AAMA,MAAa,uBAAuB;AAEpC,IAAI,2BAAyC;AAC7C,IAAI,iCAAqD;AAEzD,MAAa,4BAA4B;CACvC,IAAI,0BACF,OAAO;CAKT,2BAA2B,IAAI,MAAM,sBAAsB,EACzD,YAHiB,eAGI,EACvB,CAAC;CAED,OAAO;AACT;AAEA,MAAa,kCAAkC;CAC7C,IAAI,gCACF,OAAO;CAKT,iCAAiC,IAAI,YAAY,sBAAsB,EACrE,YAHiB,eAGI,EACvB,CAAC;CAED,OAAO;AACT;AAEA,MAAa,uBAAuB,UAClC,mBAAmB;AACrB,MAAa,wBAAwB,UACnC,oBAAoB;AAEtB,MAAa,yBAAyB,OACpC,UACqB;CAErB,OAAO,CAAC,CAAE,MADI,eACM,CAAC,CAAC,IAAI,oBAAoB,KAAK,CAAC;AACtD;AAEA,MAAa,4BAA4B,OACvC,UACqB;CAErB,OAAO,CAAC,CAAE,MADI,eACM,CAAC,CAAC,IAAI,qBAAqB,KAAK,CAAC;AACvD;AAEA,MAAa,oBAAoB,OAAO,SAKlC;CACJ,IAAI;EAEF,MAAM,MAAM,MADE,oBACQ,CAAC,CAAC,IAAI,0BAA0B,IAAI;EAC1D,OAAO,KAAK,0BAA0B,IAAI,IAAI;EAC9C,OAAO;CACT,SAAS,OAAO;EACd,OAAO,MAAM,iCAAiC,KAAK;EACnD,MAAM;CACR;AACF"}
1
+ {"version":3,"file":"translationQueue.service.mjs","names":[],"sources":["../../../src/services/translationQueue.service.ts"],"sourcesContent":["import type { ConnectionOptions } from 'node:tls';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { Queue, QueueEvents } from 'bullmq';\n\nexport const translationQueueName = `translation-queue-${process.env.NODE_ENV}`;\n\nlet translationQueueInstance: Queue | null = null;\nlet translationQueueEventsInstance: QueueEvents | null = null;\n\nexport const getTranslationQueue = () => {\n if (translationQueueInstance) {\n return translationQueueInstance;\n }\n\n const connection = getRedisClient();\n\n translationQueueInstance = new Queue(translationQueueName, {\n connection: connection as unknown as ConnectionOptions,\n });\n\n return translationQueueInstance;\n};\n\nexport const getTranslationQueueEvents = () => {\n if (translationQueueEventsInstance) {\n return translationQueueEventsInstance;\n }\n\n const connection = getRedisClient();\n\n translationQueueEventsInstance = new QueueEvents(translationQueueName, {\n connection: connection as unknown as ConnectionOptions,\n });\n\n return translationQueueEventsInstance;\n};\n\nexport const translationPauseKey = (jobId: string) =>\n `translate:pause:${jobId}`;\nexport const translationCancelKey = (jobId: string) =>\n `translate:cancel:${jobId}`;\n\nexport const isTranslationJobPaused = async (\n jobId: string\n): Promise<boolean> => {\n const redis = getRedisClient();\n return !!(await redis.get(translationPauseKey(jobId)));\n};\n\nexport const isTranslationJobCancelled = async (\n jobId: string\n): Promise<boolean> => {\n const redis = getRedisClient();\n return !!(await redis.get(translationCancelKey(jobId)));\n};\n\nexport const addTranslationJob = async (data: {\n dictionaryTargets: { dictionaryId: string; locales: Locale[] }[];\n projectId: string;\n userId: string;\n mode?: 'complete' | 'review';\n}) => {\n try {\n const queue = getTranslationQueue();\n const job = await queue.add('translate-dictionaries', data);\n logger.info(`Translation job added: ${job.id}`);\n return job;\n } catch (error) {\n logger.error('Error adding translation job:', error);\n throw error;\n }\n};\n"],"mappings":";;;;;AAMA,MAAa,uBAAuB;AAEpC,IAAI,2BAAyC;AAC7C,IAAI,iCAAqD;AAEzD,MAAa,4BAA4B;AACvC,KAAI,yBACF,QAAO;AAKT,4BAA2B,IAAI,MAAM,sBAAsB,EACzD,YAHiB,gBAGK,EACvB,CAAC;AAEF,QAAO;;AAGT,MAAa,kCAAkC;AAC7C,KAAI,+BACF,QAAO;AAKT,kCAAiC,IAAI,YAAY,sBAAsB,EACrE,YAHiB,gBAGK,EACvB,CAAC;AAEF,QAAO;;AAGT,MAAa,uBAAuB,UAClC,mBAAmB;AACrB,MAAa,wBAAwB,UACnC,oBAAoB;AAEtB,MAAa,yBAAyB,OACpC,UACqB;AAErB,QAAO,CAAC,CAAE,MADI,gBACO,CAAC,IAAI,oBAAoB,MAAM,CAAC;;AAGvD,MAAa,4BAA4B,OACvC,UACqB;AAErB,QAAO,CAAC,CAAE,MADI,gBACO,CAAC,IAAI,qBAAqB,MAAM,CAAC;;AAGxD,MAAa,oBAAoB,OAAO,SAKlC;AACJ,KAAI;EAEF,MAAM,MAAM,MADE,qBACS,CAAC,IAAI,0BAA0B,KAAK;AAC3D,SAAO,KAAK,0BAA0B,IAAI,KAAK;AAC/C,SAAO;UACA,OAAO;AACd,SAAO,MAAM,iCAAiC,MAAM;AACpD,QAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"translationWorker.service.mjs","names":["userService.getUserById","projectService.getProjectById","dictionaryService.getDictionaryById","dictionaryService.incrementVersion","dictionaryService.updateDictionaryById"],"sources":["../../../src/services/translationWorker.service.ts"],"sourcesContent":["import type { ConnectionOptions } from 'node:tls';\nimport * as eventListener from '@controllers/eventListener.controller';\nimport { type AIOptions, getAIConfig } from '@intlayer/ai';\nimport { DEFAULT_LOCALE } from '@intlayer/config/defaultValues';\nimport {\n getFilterMissingTranslationsDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport * as projectService from '@services/project.service';\nimport * as userService from '@services/user.service';\nimport {\n AbortError,\n translateDictionaryDB,\n} from '@utils/AI/translateDictionaryDB';\nimport { mapDictionaryToAPI } from '@utils/mapper/dictionary';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { type Job, Worker } from 'bullmq';\nimport {\n isTranslationJobCancelled,\n isTranslationJobPaused,\n translationQueueName,\n} from './translationQueue.service';\n\ntype TranslationJobData = {\n dictionaryIds?: string[];\n dictionaryKeys?: string[];\n targetLocales?: Locale[];\n dictionaryTargets?: { dictionaryId: string; locales: Locale[] }[];\n projectId: string;\n userId: string;\n mode?: 'complete' | 'review';\n};\n\nexport type TranslationJobProgress = {\n percentage: number;\n completedKeys: string[];\n failedKeys: string[];\n currentKey: string | null;\n /** Fine-grained chunk info emitted by translateDictionaryDB */\n currentLocale: string | null;\n currentChunk: number | null;\n totalChunks: number | null;\n};\n\nconst emitProgress = async (\n job: Job<TranslationJobData>,\n progress: TranslationJobProgress\n) => {\n await job.updateProgress(progress as unknown as number);\n};\n\nexport const processTranslationJob = async (job: Job<TranslationJobData>) => {\n const {\n dictionaryTargets,\n dictionaryIds,\n targetLocales,\n projectId,\n userId,\n mode = 'complete',\n } = job.data;\n\n // Migration / compatibility: if dictionaryTargets is missing, rebuild it from flat list\n const targets: { dictionaryId: string; locales: Locale[] }[] =\n dictionaryTargets ||\n (dictionaryIds as string[])?.map((id) => ({\n dictionaryId: id,\n locales: targetLocales as Locale[],\n })) ||\n [];\n\n logger.info(`Processing translation job ${job.id} for project ${projectId}`);\n\n const user = await userService.getUserById(userId);\n const project = await projectService.getProjectById(projectId);\n\n if (!user || !project) {\n throw new Error('User or Project not found');\n }\n\n const projectAIOptions = project.configuration?.ai as AIOptions | undefined;\n\n const aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n true\n );\n\n const totalDictionaries = targets.length;\n let processedCount = 0;\n const completedKeys: string[] = [];\n const failedKeys: string[] = [];\n\n for (const target of targets) {\n const { dictionaryId, locales: taskTargetLocales } = target;\n\n // Respect pause: spin-wait until resumed or cancelled\n while (await isTranslationJobPaused(job.id!)) {\n if (await isTranslationJobCancelled(job.id!)) break;\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n // Stop gracefully if cancelled\n if (await isTranslationJobCancelled(job.id!)) {\n throw new Error('Cancelled by user');\n }\n\n /** Checked between every AI chunk – aborts the current dictionary cleanly */\n const shouldStop = async () => {\n if (await isTranslationJobCancelled(job.id!)) return true;\n if (await isTranslationJobPaused(job.id!)) return true;\n return false;\n };\n let dictionaryKey = dictionaryId;\n try {\n const dictionary =\n await dictionaryService.getDictionaryById(dictionaryId);\n dictionaryKey = dictionary.key;\n\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: dictionaryKey,\n currentLocale: null,\n currentChunk: null,\n totalChunks: null,\n });\n\n const versionList = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const dictionaryContentNode = dictionary.content.get(lastVersion);\n\n if (!dictionaryContentNode) {\n logger.warn(`No content found for dictionary ${dictionary.key}`);\n failedKeys.push(dictionaryKey);\n processedCount++;\n continue;\n }\n\n const sourceContent = dictionaryContentNode.content;\n\n const sourceLocale =\n project.configuration?.internationalization?.defaultLocale ||\n DEFAULT_LOCALE;\n\n // Translate per locale, sending only the content that is actually missing.\n // This mirrors the CLI's complete-mode logic:\n // 1. getFilterMissingTranslationsDictionary – strips t() nodes that\n // already have this locale, so the AI never sees translated content.\n // 2. getPerLocaleDictionary – extracts flat source-locale text for the\n // missing nodes (no multilingual wrappers sent to the AI).\n // 3. insertContentInDictionary – puts the AI result back into the\n // multilingual structure correctly.\n const translationResult: Partial<Record<Locale, unknown>> = {};\n\n for (const targetLocale of taskTargetLocales) {\n // In 'complete' mode: send only the missing nodes to the AI.\n // In 'review' mode: send everything so the AI can re-translate / improve.\n let contentToTranslate: Record<string, unknown>;\n\n if (mode === 'review') {\n const sourceForAll = getPerLocaleDictionary(\n {\n key: dictionary.key,\n content: sourceContent as any,\n schema: undefined,\n },\n sourceLocale as Locale\n );\n contentToTranslate = sourceForAll.content as Record<string, unknown>;\n } else {\n const missingForLocale = getFilterMissingTranslationsDictionary(\n {\n key: dictionary.key,\n content: sourceContent as any,\n schema: undefined,\n },\n targetLocale\n );\n const sourceForMissing = getPerLocaleDictionary(\n missingForLocale,\n sourceLocale as Locale\n );\n contentToTranslate = sourceForMissing.content as Record<\n string,\n unknown\n >;\n }\n\n if (\n !contentToTranslate ||\n Object.keys(contentToTranslate).length === 0\n ) {\n logger.info(\n `Dictionary ${dictionary.key}: locale ${targetLocale} already complete, skipping`\n );\n continue;\n }\n\n const localeResult = await translateDictionaryDB({\n content: contentToTranslate as any,\n sourceLocale: sourceLocale as Locale,\n targetLocales: [targetLocale],\n aiConfig,\n mode,\n dictionaryDescription: dictionary.description,\n shouldStop,\n onChunkStart: async ({ locale, chunkIndex, totalChunks }) => {\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: dictionaryKey,\n currentLocale: locale,\n currentChunk: chunkIndex + 1,\n totalChunks,\n });\n },\n });\n\n if (localeResult[targetLocale]) {\n translationResult[targetLocale] = localeResult[targetLocale];\n }\n }\n\n if (Object.keys(translationResult).length === 0) {\n logger.info(\n `Dictionary ${dictionary.key}: all target locales already complete`\n );\n completedKeys.push(dictionaryKey);\n processedCount++;\n continue;\n }\n\n // Fetch fresh DB content before writing to avoid overwriting concurrent edits.\n const currentDictionary =\n await dictionaryService.getDictionaryById(dictionaryId);\n const currentContentNode = currentDictionary.content.get(lastVersion)!;\n\n // Insert each locale's translated content into the multilingual structure.\n let updatedDict: { key: string; content: any } = {\n key: dictionary.key,\n content: currentContentNode.content,\n };\n\n for (const [locale, localeContent] of Object.entries(translationResult)) {\n updatedDict = insertContentInDictionary(\n updatedDict as any,\n localeContent as any,\n locale as Locale\n );\n }\n\n const newVersion = dictionaryService.incrementVersion(currentDictionary);\n const updatedContentMap = new Map(currentDictionary.content);\n updatedContentMap.set(newVersion, {\n content: updatedDict.content as any,\n });\n\n const updatedDictionary = await dictionaryService.updateDictionaryById(\n dictionaryId,\n { content: updatedContentMap }\n );\n\n eventListener.sendDictionaryUpdate([\n {\n dictionary: mapDictionaryToAPI(updatedDictionary),\n status: 'UPDATED',\n },\n ]);\n\n completedKeys.push(dictionaryKey);\n } catch (error) {\n if (error instanceof AbortError) {\n // Paused or cancelled mid-chunk – stop the loop without marking as failed\n logger.info(\n `Translation job ${job.id} aborted mid-chunk (pause/cancel)`\n );\n break;\n }\n logger.error(`Error translating dictionary ${dictionaryId}:`, error);\n failedKeys.push(dictionaryKey);\n }\n\n processedCount++;\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: null,\n currentLocale: null,\n currentChunk: null,\n totalChunks: null,\n });\n }\n};\n\nlet translationWorker: Worker<TranslationJobData> | null = null;\n\nexport const startTranslationWorker = () => {\n if (translationWorker) return translationWorker;\n\n const connection = getRedisClient();\n\n translationWorker = new Worker<TranslationJobData>(\n translationQueueName,\n processTranslationJob,\n {\n connection: connection as unknown as ConnectionOptions,\n concurrency: 5,\n }\n );\n\n translationWorker.on('completed', (job) => {\n logger.info(`Translation job ${job.id} completed`);\n });\n\n translationWorker.on('failed', (job, err) => {\n logger.error(`Translation job ${job?.id} failed:`, err);\n });\n\n return translationWorker;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"translationWorker.service.mjs","names":["userService.getUserById","projectService.getProjectById","dictionaryService.getDictionaryById","dictionaryService.incrementVersion","dictionaryService.updateDictionaryById"],"sources":["../../../src/services/translationWorker.service.ts"],"sourcesContent":["import type { ConnectionOptions } from 'node:tls';\nimport * as eventListener from '@controllers/eventListener.controller';\nimport { type AIOptions, getAIConfig } from '@intlayer/ai';\nimport { DEFAULT_LOCALE } from '@intlayer/config/defaultValues';\nimport {\n getFilterMissingTranslationsDictionary,\n getPerLocaleDictionary,\n insertContentInDictionary,\n} from '@intlayer/core/plugins';\nimport type { Locale } from '@intlayer/types/allLocales';\nimport { logger } from '@logger';\nimport * as dictionaryService from '@services/dictionary.service';\nimport * as projectService from '@services/project.service';\nimport * as userService from '@services/user.service';\nimport {\n AbortError,\n translateDictionaryDB,\n} from '@utils/AI/translateDictionaryDB';\nimport { mapDictionaryToAPI } from '@utils/mapper/dictionary';\nimport { getRedisClient } from '@utils/redis/connectRedis';\nimport { type Job, Worker } from 'bullmq';\nimport {\n isTranslationJobCancelled,\n isTranslationJobPaused,\n translationQueueName,\n} from './translationQueue.service';\n\ntype TranslationJobData = {\n dictionaryIds?: string[];\n dictionaryKeys?: string[];\n targetLocales?: Locale[];\n dictionaryTargets?: { dictionaryId: string; locales: Locale[] }[];\n projectId: string;\n userId: string;\n mode?: 'complete' | 'review';\n};\n\nexport type TranslationJobProgress = {\n percentage: number;\n completedKeys: string[];\n failedKeys: string[];\n currentKey: string | null;\n /** Fine-grained chunk info emitted by translateDictionaryDB */\n currentLocale: string | null;\n currentChunk: number | null;\n totalChunks: number | null;\n};\n\nconst emitProgress = async (\n job: Job<TranslationJobData>,\n progress: TranslationJobProgress\n) => {\n await job.updateProgress(progress as unknown as number);\n};\n\nexport const processTranslationJob = async (job: Job<TranslationJobData>) => {\n const {\n dictionaryTargets,\n dictionaryIds,\n targetLocales,\n projectId,\n userId,\n mode = 'complete',\n } = job.data;\n\n // Migration / compatibility: if dictionaryTargets is missing, rebuild it from flat list\n const targets: { dictionaryId: string; locales: Locale[] }[] =\n dictionaryTargets ||\n (dictionaryIds as string[])?.map((id) => ({\n dictionaryId: id,\n locales: targetLocales as Locale[],\n })) ||\n [];\n\n logger.info(`Processing translation job ${job.id} for project ${projectId}`);\n\n const user = await userService.getUserById(userId);\n const project = await projectService.getProjectById(projectId);\n\n if (!user || !project) {\n throw new Error('User or Project not found');\n }\n\n const projectAIOptions = project.configuration?.ai as AIOptions | undefined;\n\n const aiConfig = await getAIConfig(\n {\n userOptions: {},\n projectOptions: projectAIOptions,\n accessType: ['registered_user', 'apiKey'],\n },\n true\n );\n\n const totalDictionaries = targets.length;\n let processedCount = 0;\n const completedKeys: string[] = [];\n const failedKeys: string[] = [];\n\n for (const target of targets) {\n const { dictionaryId, locales: taskTargetLocales } = target;\n\n // Respect pause: spin-wait until resumed or cancelled\n while (await isTranslationJobPaused(job.id!)) {\n if (await isTranslationJobCancelled(job.id!)) break;\n await new Promise((resolve) => setTimeout(resolve, 2000));\n }\n\n // Stop gracefully if cancelled\n if (await isTranslationJobCancelled(job.id!)) {\n throw new Error('Cancelled by user');\n }\n\n /** Checked between every AI chunk – aborts the current dictionary cleanly */\n const shouldStop = async () => {\n if (await isTranslationJobCancelled(job.id!)) return true;\n if (await isTranslationJobPaused(job.id!)) return true;\n return false;\n };\n let dictionaryKey = dictionaryId;\n try {\n const dictionary =\n await dictionaryService.getDictionaryById(dictionaryId);\n dictionaryKey = dictionary.key;\n\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: dictionaryKey,\n currentLocale: null,\n currentChunk: null,\n totalChunks: null,\n });\n\n const versionList = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versionList[versionList.length - 1] || 'v1';\n const dictionaryContentNode = dictionary.content.get(lastVersion);\n\n if (!dictionaryContentNode) {\n logger.warn(`No content found for dictionary ${dictionary.key}`);\n failedKeys.push(dictionaryKey);\n processedCount++;\n continue;\n }\n\n const sourceContent = dictionaryContentNode.content;\n\n const sourceLocale =\n project.configuration?.internationalization?.defaultLocale ||\n DEFAULT_LOCALE;\n\n // Translate per locale, sending only the content that is actually missing.\n // This mirrors the CLI's complete-mode logic:\n // 1. getFilterMissingTranslationsDictionary – strips t() nodes that\n // already have this locale, so the AI never sees translated content.\n // 2. getPerLocaleDictionary – extracts flat source-locale text for the\n // missing nodes (no multilingual wrappers sent to the AI).\n // 3. insertContentInDictionary – puts the AI result back into the\n // multilingual structure correctly.\n const translationResult: Partial<Record<Locale, unknown>> = {};\n\n for (const targetLocale of taskTargetLocales) {\n // In 'complete' mode: send only the missing nodes to the AI.\n // In 'review' mode: send everything so the AI can re-translate / improve.\n let contentToTranslate: Record<string, unknown>;\n\n if (mode === 'review') {\n const sourceForAll = getPerLocaleDictionary(\n {\n key: dictionary.key,\n content: sourceContent as any,\n schema: undefined,\n },\n sourceLocale as Locale\n );\n contentToTranslate = sourceForAll.content as Record<string, unknown>;\n } else {\n const missingForLocale = getFilterMissingTranslationsDictionary(\n {\n key: dictionary.key,\n content: sourceContent as any,\n schema: undefined,\n },\n targetLocale\n );\n const sourceForMissing = getPerLocaleDictionary(\n missingForLocale,\n sourceLocale as Locale\n );\n contentToTranslate = sourceForMissing.content as Record<\n string,\n unknown\n >;\n }\n\n if (\n !contentToTranslate ||\n Object.keys(contentToTranslate).length === 0\n ) {\n logger.info(\n `Dictionary ${dictionary.key}: locale ${targetLocale} already complete, skipping`\n );\n continue;\n }\n\n const localeResult = await translateDictionaryDB({\n content: contentToTranslate as any,\n sourceLocale: sourceLocale as Locale,\n targetLocales: [targetLocale],\n aiConfig,\n mode,\n dictionaryDescription: dictionary.description,\n shouldStop,\n onChunkStart: async ({ locale, chunkIndex, totalChunks }) => {\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: dictionaryKey,\n currentLocale: locale,\n currentChunk: chunkIndex + 1,\n totalChunks,\n });\n },\n });\n\n if (localeResult[targetLocale]) {\n translationResult[targetLocale] = localeResult[targetLocale];\n }\n }\n\n if (Object.keys(translationResult).length === 0) {\n logger.info(\n `Dictionary ${dictionary.key}: all target locales already complete`\n );\n completedKeys.push(dictionaryKey);\n processedCount++;\n continue;\n }\n\n // Fetch fresh DB content before writing to avoid overwriting concurrent edits.\n const currentDictionary =\n await dictionaryService.getDictionaryById(dictionaryId);\n const currentContentNode = currentDictionary.content.get(lastVersion)!;\n\n // Insert each locale's translated content into the multilingual structure.\n let updatedDict: { key: string; content: any } = {\n key: dictionary.key,\n content: currentContentNode.content,\n };\n\n for (const [locale, localeContent] of Object.entries(translationResult)) {\n updatedDict = insertContentInDictionary(\n updatedDict as any,\n localeContent as any,\n locale as Locale\n );\n }\n\n const newVersion = dictionaryService.incrementVersion(currentDictionary);\n const updatedContentMap = new Map(currentDictionary.content);\n updatedContentMap.set(newVersion, {\n content: updatedDict.content as any,\n });\n\n const updatedDictionary = await dictionaryService.updateDictionaryById(\n dictionaryId,\n { content: updatedContentMap }\n );\n\n eventListener.sendDictionaryUpdate([\n {\n dictionary: mapDictionaryToAPI(updatedDictionary),\n status: 'UPDATED',\n },\n ]);\n\n completedKeys.push(dictionaryKey);\n } catch (error) {\n if (error instanceof AbortError) {\n // Paused or cancelled mid-chunk – stop the loop without marking as failed\n logger.info(\n `Translation job ${job.id} aborted mid-chunk (pause/cancel)`\n );\n break;\n }\n logger.error(`Error translating dictionary ${dictionaryId}:`, error);\n failedKeys.push(dictionaryKey);\n }\n\n processedCount++;\n await emitProgress(job, {\n percentage: (processedCount / totalDictionaries) * 100,\n completedKeys,\n failedKeys,\n currentKey: null,\n currentLocale: null,\n currentChunk: null,\n totalChunks: null,\n });\n }\n};\n\nlet translationWorker: Worker<TranslationJobData> | null = null;\n\nexport const startTranslationWorker = () => {\n if (translationWorker) return translationWorker;\n\n const connection = getRedisClient();\n\n translationWorker = new Worker<TranslationJobData>(\n translationQueueName,\n processTranslationJob,\n {\n connection: connection as unknown as ConnectionOptions,\n concurrency: 5,\n }\n );\n\n translationWorker.on('completed', (job) => {\n logger.info(`Translation job ${job.id} completed`);\n });\n\n translationWorker.on('failed', (job, err) => {\n logger.error(`Translation job ${job?.id} failed:`, err);\n });\n\n return translationWorker;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"avatarUpload.service.mjs","names":[],"sources":["../../../../src/services/user/avatarUpload.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { resizeImage } from '@utils/image/resizeImage';\nimport { getS3Client } from '@utils/s3/s3Client';\n\nconst ALLOWED_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\nconst MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB pre-resize\n\nexport type AvatarValidationError = 'UNSUPPORTED_TYPE' | 'TOO_LARGE';\n\nexport const validateAvatarUpload = (\n contentType: string,\n contentLength: number\n): AvatarValidationError | null => {\n if (!ALLOWED_MIME_TYPES.includes(contentType)) return 'UNSUPPORTED_TYPE';\n if (contentLength > MAX_SIZE_BYTES) return 'TOO_LARGE';\n return null;\n};\n\nconst getAvatarKey = (userId: string): string => `avatars/${userId}.jpg`;\n\nexport const uploadUserAvatar = async (\n buffer: Buffer,\n userId: string\n): Promise<string> => {\n const { buffer: resized, contentType } = await resizeImage(buffer, {\n width: 256,\n height: 256,\n quality: 85,\n });\n\n const key = getAvatarKey(userId);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: resized,\n ContentType: contentType,\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\nexport const deleteUserAvatar = async (imageUrl: string): Promise<void> => {\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : null;\n\n // Only delete avatars we own — skip external URLs (Google, GitHub, etc.)\n if (!key?.startsWith('avatars/')) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;AAIA,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;AACF;AACA,MAAM,iBAAiB,KAAK,OAAO;AAInC,MAAa,wBACX,aACA,kBACiC;CACjC,IAAI,CAAC,mBAAmB,SAAS,WAAW,GAAG,OAAO;CACtD,IAAI,gBAAgB,gBAAgB,OAAO;CAC3C,OAAO;AACT;AAEA,MAAM,gBAAgB,WAA2B,WAAW,OAAO;AAEnE,MAAa,mBAAmB,OAC9B,QACA,WACoB;CACpB,MAAM,EAAE,QAAQ,SAAS,gBAAgB,MAAM,YAAY,QAAQ;EACjE,OAAO;EACP,QAAQ;EACR,SAAS;CACX,CAAC;CAED,MAAM,MAAM,aAAa,MAAM;CAG/B,MAFiB,YAEJ,CAAC,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;CACf,CAAC,CACH;CAEA,OAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;AACzC;AAEA,MAAa,mBAAmB,OAAO,aAAoC;CACzE,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,SAAS,IACrC,SAAS,MAAM,UAAU,SAAS,CAAC,IACnC;CAGJ,IAAI,CAAC,KAAK,WAAW,UAAU,GAAG;CAGlC,MADiB,YACJ,CAAC,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;CACP,CAAC,CACH;AACF"}
1
+ {"version":3,"file":"avatarUpload.service.mjs","names":[],"sources":["../../../../src/services/user/avatarUpload.service.ts"],"sourcesContent":["import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';\nimport { resizeImage } from '@utils/image/resizeImage';\nimport { getS3Client } from '@utils/s3/s3Client';\n\nconst ALLOWED_MIME_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n];\nconst MAX_SIZE_BYTES = 20 * 1024 * 1024; // 20 MB pre-resize\n\nexport type AvatarValidationError = 'UNSUPPORTED_TYPE' | 'TOO_LARGE';\n\nexport const validateAvatarUpload = (\n contentType: string,\n contentLength: number\n): AvatarValidationError | null => {\n if (!ALLOWED_MIME_TYPES.includes(contentType)) return 'UNSUPPORTED_TYPE';\n if (contentLength > MAX_SIZE_BYTES) return 'TOO_LARGE';\n return null;\n};\n\nconst getAvatarKey = (userId: string): string => `avatars/${userId}.jpg`;\n\nexport const uploadUserAvatar = async (\n buffer: Buffer,\n userId: string\n): Promise<string> => {\n const { buffer: resized, contentType } = await resizeImage(buffer, {\n width: 256,\n height: 256,\n quality: 85,\n });\n\n const key = getAvatarKey(userId);\n const s3Client = getS3Client();\n\n await s3Client.send(\n new PutObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n Body: resized,\n ContentType: contentType,\n })\n );\n\n return `${process.env.S3_PUBLIC_URL}/${key}`;\n};\n\nexport const deleteUserAvatar = async (imageUrl: string): Promise<void> => {\n const publicUrl = process.env.S3_PUBLIC_URL ?? '';\n const key = imageUrl.startsWith(publicUrl)\n ? imageUrl.slice(publicUrl.length + 1)\n : null;\n\n // Only delete avatars we own — skip external URLs (Google, GitHub, etc.)\n if (!key?.startsWith('avatars/')) return;\n\n const s3Client = getS3Client();\n await s3Client.send(\n new DeleteObjectCommand({\n Bucket: process.env.S3_BUCKET_NAME,\n Key: key,\n })\n );\n};\n"],"mappings":";;;;;AAIA,MAAM,qBAAqB;CACzB;CACA;CACA;CACA;CACD;AACD,MAAM,iBAAiB,KAAK,OAAO;AAInC,MAAa,wBACX,aACA,kBACiC;AACjC,KAAI,CAAC,mBAAmB,SAAS,YAAY,CAAE,QAAO;AACtD,KAAI,gBAAgB,eAAgB,QAAO;AAC3C,QAAO;;AAGT,MAAM,gBAAgB,WAA2B,WAAW,OAAO;AAEnE,MAAa,mBAAmB,OAC9B,QACA,WACoB;CACpB,MAAM,EAAE,QAAQ,SAAS,gBAAgB,MAAM,YAAY,QAAQ;EACjE,OAAO;EACP,QAAQ;EACR,SAAS;EACV,CAAC;CAEF,MAAM,MAAM,aAAa,OAAO;AAGhC,OAFiB,aAEH,CAAC,KACb,IAAI,iBAAiB;EACnB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACL,MAAM;EACN,aAAa;EACd,CAAC,CACH;AAED,QAAO,GAAG,QAAQ,IAAI,cAAc,GAAG;;AAGzC,MAAa,mBAAmB,OAAO,aAAoC;CACzE,MAAM,YAAY,QAAQ,IAAI,iBAAiB;CAC/C,MAAM,MAAM,SAAS,WAAW,UAAU,GACtC,SAAS,MAAM,UAAU,SAAS,EAAE,GACpC;AAGJ,KAAI,CAAC,KAAK,WAAW,WAAW,CAAE;AAGlC,OADiB,aACH,CAAC,KACb,IAAI,oBAAoB;EACtB,QAAQ,QAAQ,IAAI;EACpB,KAAK;EACN,CAAC,CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"user.service.mjs","names":[],"sources":["../../../src/services/user.service.ts"],"sourcesContent":["import { UserModel } from '@schemas/user.schema';\nimport { GenericError } from '@utils/errors';\nimport type { UserFilters } from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport {\n type FieldsToCheck,\n type UserFields,\n validateUser,\n} from '@utils/validation/validateUser';\nimport type { Types } from 'mongoose';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\n\n/**\n * Creates a new user with password in the database and hashes the password.\n * @param user - User object with password not hashed.\n * @returns Created user object.\n */\nexport const createUser = async (\n user: Partial<User>\n): Promise<UserDocument> => {\n const fieldsToCheck: FieldsToCheck[] = ['email'];\n\n const errors = validateUser(user, fieldsToCheck);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userEmail: user.email,\n errors,\n });\n }\n\n const newUser: UserDocument = await UserModel.create(user);\n\n if (!newUser) {\n throw new GenericError('USER_CREATION_FAILED', { userEmail: user.email });\n }\n\n return newUser;\n};\n\n/**\n * Retrieves a user by email.\n * @param email - User's email.\n * @returns User object or null if no user was found.\n */\nexport const getUserByEmail = async (\n email: string\n): Promise<UserDocument | null> => {\n return await UserModel.findOne({ email: String(email) });\n};\n\n/**\n * Retrieves users list by email.\n * @param emails - Users email.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByEmails = async (\n emails: string[]\n): Promise<UserDocument[] | null> => {\n return await UserModel.find({ email: { $in: emails } });\n};\n\n/**\n * Checks if a user exists by email.\n * @param email - User's email.\n * @returns True if the user exists, false otherwise.\n */\nexport const checkUserExists = async (email: string): Promise<boolean> => {\n const user = await UserModel.exists({ email });\n return user !== null;\n};\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUserById = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument | null> => await UserModel.findById(userId);\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByIds = async (\n userIds: (string | Types.ObjectId)[]\n): Promise<UserDocument[] | null> =>\n await UserModel.find({ _id: { $in: userIds } });\n\n/**\n * Finds users based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @param sortOptions - Sorting options.\n * @returns List of users matching the filters.\n */\nexport const findUsers = async (\n filters: UserFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<UserDocument[]> => {\n let query = UserModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Counts the total number of users that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of users.\n */\nexport const countUsers = async (filters: UserFilters): Promise<number> => {\n const count = await UserModel.countDocuments(filters);\n\n if (typeof count === 'undefined') {\n throw new GenericError('USER_COUNT_FAILED');\n }\n\n return count;\n};\n\n/**\n * Updates a user's information.\n * @param user - The user object.\n * @param updates - The updates to apply to the user.\n * @returns The updated user.\n */\nexport const updateUserById = async (\n userId: string | Types.ObjectId,\n updates: Partial<UserAPI>\n): Promise<UserDocument> => {\n const { id, ...updatesWithoutId } = updates;\n\n const keyToValidate = Object.keys(updatesWithoutId) as UserFields;\n const errors = validateUser(updatesWithoutId, keyToValidate);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userId,\n errors,\n });\n }\n\n const result = await UserModel.updateOne(\n { _id: userId },\n { $set: updatesWithoutId }\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('USER_UPDATE_FAILED', { userId });\n }\n\n const updatedUser = await UserModel.findById(userId);\n\n if (!updatedUser) {\n throw new GenericError('USER_UPDATED_USER_NOT_FOUND', { userId });\n }\n\n return updatedUser;\n};\n\n/**\n * Deletes a user from the database.\n * @param userId - The user object.\n * @returns\n */\nexport const deleteUser = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument> => {\n await getUserById(userId);\n\n const user = await UserModel.findByIdAndDelete(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', { userId });\n }\n\n return user;\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,aAAa,OACxB,SAC0B;CAG1B,MAAM,SAAS,aAAa,MAAM,CAFM,OAEM,CAAC;CAE/C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,uBAAuB;EAC5C,WAAW,KAAK;EAChB;CACF,CAAC;CAGH,MAAM,UAAwB,MAAM,UAAU,OAAO,IAAI;CAEzD,IAAI,CAAC,SACH,MAAM,IAAI,aAAa,wBAAwB,EAAE,WAAW,KAAK,MAAM,CAAC;CAG1E,OAAO;AACT;;;;;;AAOA,MAAa,iBAAiB,OAC5B,UACiC;CACjC,OAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;AACzD;;;;;;AAOA,MAAa,mBAAmB,OAC9B,WACmC;CACnC,OAAO,MAAM,UAAU,KAAK,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,CAAC;AACxD;;;;;;AAOA,MAAa,kBAAkB,OAAO,UAAoC;CAExE,OAAO,MADY,UAAU,OAAO,EAAE,MAAM,CAAC,MAC7B;AAClB;;;;;;AAOA,MAAa,cAAc,OACzB,WACiC,MAAM,UAAU,SAAS,MAAM;;;;;;AAOlE,MAAa,gBAAgB,OAC3B,YAEA,MAAM,UAAU,KAAK,EAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,CAAC;;;;;;;;;AAUhD,MAAa,YAAY,OACvB,SACA,MACA,OACA,gBAC4B;CAC5B,IAAI,QAAQ,UAAU,KAAK,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK;CAE1D,IAAI,eAAe,OAAO,KAAK,WAAW,CAAC,CAAC,SAAS,GACnD,QAAQ,MAAM,KAAK,WAAW;CAGhC,OAAO,MAAM;AACf;;;;;;AAOA,MAAa,aAAa,OAAO,YAA0C;CACzE,MAAM,QAAQ,MAAM,UAAU,eAAe,OAAO;CAEpD,IAAI,OAAO,UAAU,aACnB,MAAM,IAAI,aAAa,mBAAmB;CAG5C,OAAO;AACT;;;;;;;AAQA,MAAa,iBAAiB,OAC5B,QACA,YAC0B;CAC1B,MAAM,EAAE,IAAI,GAAG,qBAAqB;CAGpC,MAAM,SAAS,aAAa,kBADN,OAAO,KAAK,gBACwB,CAAC;CAE3D,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,MAAM,IAAI,aAAa,uBAAuB;EAC5C;EACA;CACF,CAAC;CAQH,KAAI,MALiB,UAAU,UAC7B,EAAE,KAAK,OAAO,GACd,EAAE,MAAM,iBAAiB,CAC3B,EAEU,CAAC,iBAAiB,GAC1B,MAAM,IAAI,aAAa,sBAAsB,EAAE,OAAO,CAAC;CAGzD,MAAM,cAAc,MAAM,UAAU,SAAS,MAAM;CAEnD,IAAI,CAAC,aACH,MAAM,IAAI,aAAa,+BAA+B,EAAE,OAAO,CAAC;CAGlE,OAAO;AACT;;;;;;AAOA,MAAa,aAAa,OACxB,WAC0B;CAC1B,MAAM,YAAY,MAAM;CAExB,MAAM,OAAO,MAAM,UAAU,kBAAkB,MAAM;CAErD,IAAI,CAAC,MACH,MAAM,IAAI,aAAa,kBAAkB,EAAE,OAAO,CAAC;CAGrD,OAAO;AACT"}
1
+ {"version":3,"file":"user.service.mjs","names":[],"sources":["../../../src/services/user.service.ts"],"sourcesContent":["import { UserModel } from '@schemas/user.schema';\nimport { GenericError } from '@utils/errors';\nimport type { UserFilters } from '@utils/filtersAndPagination/getUserFiltersAndPagination';\nimport {\n type FieldsToCheck,\n type UserFields,\n validateUser,\n} from '@utils/validation/validateUser';\nimport type { Types } from 'mongoose';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\n\n/**\n * Creates a new user with password in the database and hashes the password.\n * @param user - User object with password not hashed.\n * @returns Created user object.\n */\nexport const createUser = async (\n user: Partial<User>\n): Promise<UserDocument> => {\n const fieldsToCheck: FieldsToCheck[] = ['email'];\n\n const errors = validateUser(user, fieldsToCheck);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userEmail: user.email,\n errors,\n });\n }\n\n const newUser: UserDocument = await UserModel.create(user);\n\n if (!newUser) {\n throw new GenericError('USER_CREATION_FAILED', { userEmail: user.email });\n }\n\n return newUser;\n};\n\n/**\n * Retrieves a user by email.\n * @param email - User's email.\n * @returns User object or null if no user was found.\n */\nexport const getUserByEmail = async (\n email: string\n): Promise<UserDocument | null> => {\n return await UserModel.findOne({ email: String(email) });\n};\n\n/**\n * Retrieves users list by email.\n * @param emails - Users email.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByEmails = async (\n emails: string[]\n): Promise<UserDocument[] | null> => {\n return await UserModel.find({ email: { $in: emails } });\n};\n\n/**\n * Checks if a user exists by email.\n * @param email - User's email.\n * @returns True if the user exists, false otherwise.\n */\nexport const checkUserExists = async (email: string): Promise<boolean> => {\n const user = await UserModel.exists({ email });\n return user !== null;\n};\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUserById = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument | null> => await UserModel.findById(userId);\n\n/**\n * Retrieves a user by ID.\n * @param userId - User's ID.\n * @returns User object or null if no user was found.\n */\nexport const getUsersByIds = async (\n userIds: (string | Types.ObjectId)[]\n): Promise<UserDocument[] | null> =>\n await UserModel.find({ _id: { $in: userIds } });\n\n/**\n * Finds users based on filters and pagination options.\n * @param filters - MongoDB filter query.\n * @param skip - Number of documents to skip.\n * @param limit - Number of documents to limit.\n * @param sortOptions - Sorting options.\n * @returns List of users matching the filters.\n */\nexport const findUsers = async (\n filters: UserFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<UserDocument[]> => {\n let query = UserModel.find(filters).skip(skip).limit(limit);\n\n if (sortOptions && Object.keys(sortOptions).length > 0) {\n query = query.sort(sortOptions);\n }\n\n return await query;\n};\n\n/**\n * Counts the total number of users that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of users.\n */\nexport const countUsers = async (filters: UserFilters): Promise<number> => {\n const count = await UserModel.countDocuments(filters);\n\n if (typeof count === 'undefined') {\n throw new GenericError('USER_COUNT_FAILED');\n }\n\n return count;\n};\n\n/**\n * Updates a user's information.\n * @param user - The user object.\n * @param updates - The updates to apply to the user.\n * @returns The updated user.\n */\nexport const updateUserById = async (\n userId: string | Types.ObjectId,\n updates: Partial<UserAPI>\n): Promise<UserDocument> => {\n const { id, ...updatesWithoutId } = updates;\n\n const keyToValidate = Object.keys(updatesWithoutId) as UserFields;\n const errors = validateUser(updatesWithoutId, keyToValidate);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('USER_INVALID_FIELDS', {\n userId,\n errors,\n });\n }\n\n const result = await UserModel.updateOne(\n { _id: userId },\n { $set: updatesWithoutId }\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('USER_UPDATE_FAILED', { userId });\n }\n\n const updatedUser = await UserModel.findById(userId);\n\n if (!updatedUser) {\n throw new GenericError('USER_UPDATED_USER_NOT_FOUND', { userId });\n }\n\n return updatedUser;\n};\n\n/**\n * Deletes a user from the database.\n * @param userId - The user object.\n * @returns\n */\nexport const deleteUser = async (\n userId: string | Types.ObjectId\n): Promise<UserDocument> => {\n await getUserById(userId);\n\n const user = await UserModel.findByIdAndDelete(userId);\n\n if (!user) {\n throw new GenericError('USER_NOT_FOUND', { userId });\n }\n\n return user;\n};\n"],"mappings":";;;;;;;;;;AAgBA,MAAa,aAAa,OACxB,SAC0B;CAG1B,MAAM,SAAS,aAAa,MAAM,CAFM,QAEO,CAAC;AAEhD,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C,WAAW,KAAK;EAChB;EACD,CAAC;CAGJ,MAAM,UAAwB,MAAM,UAAU,OAAO,KAAK;AAE1D,KAAI,CAAC,QACH,OAAM,IAAI,aAAa,wBAAwB,EAAE,WAAW,KAAK,OAAO,CAAC;AAG3E,QAAO;;;;;;;AAQT,MAAa,iBAAiB,OAC5B,UACiC;AACjC,QAAO,MAAM,UAAU,QAAQ,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC;;;;;;;AAQ1D,MAAa,mBAAmB,OAC9B,WACmC;AACnC,QAAO,MAAM,UAAU,KAAK,EAAE,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;;;;;;;AAQzD,MAAa,kBAAkB,OAAO,UAAoC;AAExE,QAAO,MADY,UAAU,OAAO,EAAE,OAAO,CAAC,KAC9B;;;;;;;AAQlB,MAAa,cAAc,OACzB,WACiC,MAAM,UAAU,SAAS,OAAO;;;;;;AAOnE,MAAa,gBAAgB,OAC3B,YAEA,MAAM,UAAU,KAAK,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,CAAC;;;;;;;;;AAUjD,MAAa,YAAY,OACvB,SACA,MACA,OACA,gBAC4B;CAC5B,IAAI,QAAQ,UAAU,KAAK,QAAQ,CAAC,KAAK,KAAK,CAAC,MAAM,MAAM;AAE3D,KAAI,eAAe,OAAO,KAAK,YAAY,CAAC,SAAS,EACnD,SAAQ,MAAM,KAAK,YAAY;AAGjC,QAAO,MAAM;;;;;;;AAQf,MAAa,aAAa,OAAO,YAA0C;CACzE,MAAM,QAAQ,MAAM,UAAU,eAAe,QAAQ;AAErD,KAAI,OAAO,UAAU,YACnB,OAAM,IAAI,aAAa,oBAAoB;AAG7C,QAAO;;;;;;;;AAST,MAAa,iBAAiB,OAC5B,QACA,YAC0B;CAC1B,MAAM,EAAE,IAAI,GAAG,qBAAqB;CAGpC,MAAM,SAAS,aAAa,kBADN,OAAO,KAAK,iBACyB,CAAC;AAE5D,KAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAC/B,OAAM,IAAI,aAAa,uBAAuB;EAC5C;EACA;EACD,CAAC;AAQJ,MAAI,MALiB,UAAU,UAC7B,EAAE,KAAK,QAAQ,EACf,EAAE,MAAM,kBAAkB,CAC3B,EAEU,iBAAiB,EAC1B,OAAM,IAAI,aAAa,sBAAsB,EAAE,QAAQ,CAAC;CAG1D,MAAM,cAAc,MAAM,UAAU,SAAS,OAAO;AAEpD,KAAI,CAAC,YACH,OAAM,IAAI,aAAa,+BAA+B,EAAE,QAAQ,CAAC;AAGnE,QAAO;;;;;;;AAQT,MAAa,aAAa,OACxB,WAC0B;AAC1B,OAAM,YAAY,OAAO;CAEzB,MAAM,OAAO,MAAM,UAAU,kBAAkB,OAAO;AAEtD,KAAI,CAAC,KACH,OAAM,IAAI,aAAa,kBAAkB,EAAE,QAAQ,CAAC;AAGtD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhook.service.mjs","names":[],"sources":["../../../src/services/webhook.service.ts"],"sourcesContent":["import { createHmac } from 'node:crypto';\nimport { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\nimport type { Project } from '@/types/project.types';\n\nexport type TriggerResult = {\n target: string;\n success: boolean;\n message?: string;\n};\n\n/**\n * Main entry point to trigger all configured CI pipelines for a project\n */\nexport const triggerAll = async (\n project: Project\n): Promise<TriggerResult[]> => {\n const results: TriggerResult[] = [];\n\n // Trigger Git Provider Pipeline (if configured)\n if (project.repository && project.webhooks?.autoTriggerBuilds) {\n try {\n await triggerGitPipeline(project);\n results.push({\n target: project.repository.provider,\n success: true,\n });\n } catch (error: any) {\n logger.error(`Failed to trigger ${project.repository.provider}`, error);\n results.push({\n target: project.repository.provider,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n // Trigger Generic Webhooks (Vercel, etc.) — run in parallel to avoid timeouts\n const webhooks = (project.webhooks?.webhooks || []).filter(\n (hook) => hook.enabled\n );\n\n const webhookResults = await Promise.allSettled(\n webhooks.map((hook) => triggerGenericWebhook(hook))\n );\n\n webhooks.forEach((hook, i) => {\n const outcome = webhookResults[i];\n if (outcome.status === 'fulfilled') {\n results.push({ target: hook.name, success: true });\n } else {\n const error = outcome.reason as Error;\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n results.push({\n target: hook.name,\n success: false,\n message: error.message || String(error),\n });\n }\n });\n\n return results;\n};\n\n/**\n * Triggers a single webhook by index\n */\nexport const triggerSingleWebhook = async (\n project: Project,\n webhookIndex: number\n): Promise<TriggerResult> => {\n const webhooks = project.webhooks?.webhooks || [];\n\n if (webhookIndex < 0 || webhookIndex >= webhooks.length) {\n throw new Error(`Webhook index ${webhookIndex} is out of range`);\n }\n\n const hook = webhooks[webhookIndex];\n\n if (!hook.enabled) {\n throw new Error(`Webhook \"${hook.name}\" is disabled`);\n }\n\n try {\n await triggerGenericWebhook(hook);\n return { target: hook.name, success: true };\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n return {\n target: hook.name,\n success: false,\n message: error.message || String(error),\n };\n }\n};\n\n// Internal Helper Functions (equivalent to private static methods)\n\nconst triggerGitPipeline = async (project: Project) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository) throw new Error('No repository configured');\n\n const token = oAuth2Access?.[0]?.accessToken?.[0]; // Get the first valid token\n\n if (!token) throw new Error('No valid OAuth token found');\n\n const { provider } = repository;\n\n switch (provider) {\n case 'github':\n return triggerGithub(repository, token);\n case 'gitlab':\n return triggerGitlab(repository, token);\n case 'bitbucket':\n return triggerBitbucket(repository, token);\n default:\n throw new Error(`Unknown provider: ${provider as string}`);\n }\n};\n\nconst triggerGithub = async (repo: any, token: string) => {\n const octokit = new Octokit({\n auth: token,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Triggers a 'repository_dispatch' event\n // Workflow must listen to: types: [intlayer_cms_update]\n await octokit.repos.createDispatchEvent({\n owner: repo.owner,\n repo: repo.repository,\n event_type: 'intlayer_cms_update',\n client_payload: {\n timestamp: new Date().toISOString(),\n source: 'intlayer-cms',\n },\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n logger.info(\n `Successfully triggered GitHub Action for ${repo.owner}/${repo.repository}`\n );\n};\n\n// GitLab\nconst triggerGitlab = async (repo: any, token: string) => {\n // GitLab needs Project ID (int) or URL-encoded path \"owner/repo\"\n const projectId = encodeURIComponent(`${repo.owner}/${repo.repository}`);\n const branch = repo.branch || 'main';\n const baseUrl = repo.instanceUrl || 'https://gitlab.com';\n\n const url = `${baseUrl}/api/v4/projects/${projectId}/trigger/pipeline`;\n\n const formData = new FormData();\n formData.append('token', token); // Or a specific trigger token if stored separately\n formData.append('ref', branch);\n formData.append('variables[INTLAYER_UPDATE]', 'true');\n\n const res = await fetch(url, { method: 'POST', body: formData });\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`GitLab error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitLab pipeline for ${repo.owner}/${repo.repository}`\n );\n};\n\n// Bitbucket\nconst triggerBitbucket = async (repo: any, token: string) => {\n const workspace = repo.workspace || repo.owner; // Bitbucket uses 'workspace'\n const branch = repo.branch || 'main';\n const url = `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo.repository}/pipelines/`;\n\n const body = {\n target: {\n ref_type: 'branch',\n type: 'pipeline_ref_target',\n ref_name: branch,\n // Optional: Target a custom pipeline for security\n // selector: { type: 'custom', pattern: 'intlayer-update' }\n },\n variables: [{ key: 'INTLAYER_UPDATE', value: 'true', secured: false }],\n };\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`Bitbucket error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered Bitbucket pipeline for ${workspace}/${repo.repository}`\n );\n};\n\n// Generic Webhook\nconst triggerGenericWebhook = async (hook: any) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add secret signature if provided (for webhook verification)\n if (hook.secret) {\n // Simple HMAC-SHA256 signature (can be enhanced)\n const payload = JSON.stringify({ event: 'intlayer_cms_update' });\n const signature = createHmac('sha256', hook.secret)\n .update(payload)\n .digest('hex');\n headers['X-Intlayer-Signature'] = signature;\n }\n\n const res = await fetch(hook.url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n event: 'intlayer_cms_update',\n timestamp: new Date().toISOString(),\n }),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(\n `Webhook ${hook.name} failed: ${res.status} - ${errorText}`\n );\n }\n\n logger.info(`Successfully triggered webhook: ${hook.name}`);\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"webhook.service.mjs","names":[],"sources":["../../../src/services/webhook.service.ts"],"sourcesContent":["import { createHmac } from 'node:crypto';\nimport { logger } from '@logger';\nimport { Octokit } from '@octokit/rest';\nimport type { Project } from '@/types/project.types';\n\nexport type TriggerResult = {\n target: string;\n success: boolean;\n message?: string;\n};\n\n/**\n * Main entry point to trigger all configured CI pipelines for a project\n */\nexport const triggerAll = async (\n project: Project\n): Promise<TriggerResult[]> => {\n const results: TriggerResult[] = [];\n\n // Trigger Git Provider Pipeline (if configured)\n if (project.repository && project.webhooks?.autoTriggerBuilds) {\n try {\n await triggerGitPipeline(project);\n results.push({\n target: project.repository.provider,\n success: true,\n });\n } catch (error: any) {\n logger.error(`Failed to trigger ${project.repository.provider}`, error);\n results.push({\n target: project.repository.provider,\n success: false,\n message: error.message || String(error),\n });\n }\n }\n\n // Trigger Generic Webhooks (Vercel, etc.) — run in parallel to avoid timeouts\n const webhooks = (project.webhooks?.webhooks || []).filter(\n (hook) => hook.enabled\n );\n\n const webhookResults = await Promise.allSettled(\n webhooks.map((hook) => triggerGenericWebhook(hook))\n );\n\n webhooks.forEach((hook, i) => {\n const outcome = webhookResults[i];\n if (outcome.status === 'fulfilled') {\n results.push({ target: hook.name, success: true });\n } else {\n const error = outcome.reason as Error;\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n results.push({\n target: hook.name,\n success: false,\n message: error.message || String(error),\n });\n }\n });\n\n return results;\n};\n\n/**\n * Triggers a single webhook by index\n */\nexport const triggerSingleWebhook = async (\n project: Project,\n webhookIndex: number\n): Promise<TriggerResult> => {\n const webhooks = project.webhooks?.webhooks || [];\n\n if (webhookIndex < 0 || webhookIndex >= webhooks.length) {\n throw new Error(`Webhook index ${webhookIndex} is out of range`);\n }\n\n const hook = webhooks[webhookIndex];\n\n if (!hook.enabled) {\n throw new Error(`Webhook \"${hook.name}\" is disabled`);\n }\n\n try {\n await triggerGenericWebhook(hook);\n return { target: hook.name, success: true };\n } catch (error: any) {\n logger.error(`Failed to trigger webhook ${hook.name}`, error);\n return {\n target: hook.name,\n success: false,\n message: error.message || String(error),\n };\n }\n};\n\n// Internal Helper Functions (equivalent to private static methods)\n\nconst triggerGitPipeline = async (project: Project) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository) throw new Error('No repository configured');\n\n const token = oAuth2Access?.[0]?.accessToken?.[0]; // Get the first valid token\n\n if (!token) throw new Error('No valid OAuth token found');\n\n const { provider } = repository;\n\n switch (provider) {\n case 'github':\n return triggerGithub(repository, token);\n case 'gitlab':\n return triggerGitlab(repository, token);\n case 'bitbucket':\n return triggerBitbucket(repository, token);\n default:\n throw new Error(`Unknown provider: ${provider as string}`);\n }\n};\n\nconst triggerGithub = async (repo: any, token: string) => {\n const octokit = new Octokit({\n auth: token,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Triggers a 'repository_dispatch' event\n // Workflow must listen to: types: [intlayer_cms_update]\n await octokit.repos.createDispatchEvent({\n owner: repo.owner,\n repo: repo.repository,\n event_type: 'intlayer_cms_update',\n client_payload: {\n timestamp: new Date().toISOString(),\n source: 'intlayer-cms',\n },\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n logger.info(\n `Successfully triggered GitHub Action for ${repo.owner}/${repo.repository}`\n );\n};\n\n// GitLab\nconst triggerGitlab = async (repo: any, token: string) => {\n // GitLab needs Project ID (int) or URL-encoded path \"owner/repo\"\n const projectId = encodeURIComponent(`${repo.owner}/${repo.repository}`);\n const branch = repo.branch || 'main';\n const baseUrl = repo.instanceUrl || 'https://gitlab.com';\n\n const url = `${baseUrl}/api/v4/projects/${projectId}/trigger/pipeline`;\n\n const formData = new FormData();\n formData.append('token', token); // Or a specific trigger token if stored separately\n formData.append('ref', branch);\n formData.append('variables[INTLAYER_UPDATE]', 'true');\n\n const res = await fetch(url, { method: 'POST', body: formData });\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`GitLab error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitLab pipeline for ${repo.owner}/${repo.repository}`\n );\n};\n\n// Bitbucket\nconst triggerBitbucket = async (repo: any, token: string) => {\n const workspace = repo.workspace || repo.owner; // Bitbucket uses 'workspace'\n const branch = repo.branch || 'main';\n const url = `https://api.bitbucket.org/2.0/repositories/${workspace}/${repo.repository}/pipelines/`;\n\n const body = {\n target: {\n ref_type: 'branch',\n type: 'pipeline_ref_target',\n ref_name: branch,\n // Optional: Target a custom pipeline for security\n // selector: { type: 'custom', pattern: 'intlayer-update' }\n },\n variables: [{ key: 'INTLAYER_UPDATE', value: 'true', secured: false }],\n };\n\n const res = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(`Bitbucket error: ${res.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered Bitbucket pipeline for ${workspace}/${repo.repository}`\n );\n};\n\n// Generic Webhook\nconst triggerGenericWebhook = async (hook: any) => {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add secret signature if provided (for webhook verification)\n if (hook.secret) {\n // Simple HMAC-SHA256 signature (can be enhanced)\n const payload = JSON.stringify({ event: 'intlayer_cms_update' });\n const signature = createHmac('sha256', hook.secret)\n .update(payload)\n .digest('hex');\n headers['X-Intlayer-Signature'] = signature;\n }\n\n const res = await fetch(hook.url, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n event: 'intlayer_cms_update',\n timestamp: new Date().toISOString(),\n }),\n });\n\n if (!res.ok) {\n const errorText = await res.text();\n throw new Error(\n `Webhook ${hook.name} failed: ${res.status} - ${errorText}`\n );\n }\n\n logger.info(`Successfully triggered webhook: ${hook.name}`);\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"user.types.mjs","names":[],"sources":["../../../src/types/user.types.ts"],"sourcesContent":["import type { OmitId, RenameId } from '@utils/mongoDB/types';\nimport type { User as BetterAuthUser } from 'better-auth';\nimport type { Document, Model, ObjectIdToString, Types } from 'mongoose';\n\nexport interface UserData {\n email: string;\n name: string;\n phone?: string;\n dateOfBirth?: Date;\n image?: string;\n}\n\nexport enum EmailsList {\n NEWS_LETTER = 'newsLetter',\n}\n\nexport type User = OmitId<UserData & BetterAuthUser> & {\n id: Types.ObjectId;\n emailsList?: {\n [key in EmailsList]: boolean;\n };\n customerId?: string; // Stripe customer ID\n role?: string;\n lastLoginMethod?: 'email' | 'google' | 'github' | 'passkey';\n lang?: string;\n lastActiveOrganizationId?: string | null;\n lastActiveProjectId?: string | null;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type UserAPI = ObjectIdToString<Omit<User, 'provider' | 'session'>>;\n\nexport type UserSchema = RenameId<User>;\nexport type UserModelType = Model<User>;\nexport type UserDocument = Document<unknown, {}, User> & User;\n"],"mappings":";AAYA,IAAY,aAAL;CACL;;AACF"}
1
+ {"version":3,"file":"user.types.mjs","names":[],"sources":["../../../src/types/user.types.ts"],"sourcesContent":["import type { OmitId, RenameId } from '@utils/mongoDB/types';\nimport type { User as BetterAuthUser } from 'better-auth';\nimport type { Document, Model, ObjectIdToString, Types } from 'mongoose';\n\nexport interface UserData {\n email: string;\n name: string;\n phone?: string;\n dateOfBirth?: Date;\n image?: string;\n}\n\nexport enum EmailsList {\n NEWS_LETTER = 'newsLetter',\n}\n\nexport type User = OmitId<UserData & BetterAuthUser> & {\n id: Types.ObjectId;\n emailsList?: {\n [key in EmailsList]: boolean;\n };\n customerId?: string; // Stripe customer ID\n role?: string;\n lastLoginMethod?: 'email' | 'google' | 'github' | 'passkey';\n lang?: string;\n lastActiveOrganizationId?: string | null;\n lastActiveProjectId?: string | null;\n createdAt: Date;\n updatedAt: Date;\n};\n\nexport type UserAPI = ObjectIdToString<Omit<User, 'provider' | 'session'>>;\n\nexport type UserSchema = RenameId<User>;\nexport type UserModelType = Model<User>;\nexport type UserDocument = Document<unknown, {}, User> & User;\n"],"mappings":";AAYA,IAAY,aAAL;AACL;;KACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"askDocQuestion.mjs","names":[],"sources":["../../../../../src/utils/AI/askDocQuestion/askDocQuestion.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n type AIConfig,\n type ChatCompletionRequestMessage,\n streamText,\n} from '@intlayer/ai';\nimport { getMarkdownMetadata } from '@intlayer/core/markdown';\nimport { getBlogs, getDocs, getFrequentQuestions } from '@intlayer/docs';\nimport { logger } from '@logger';\nimport { OpenAI } from 'openai';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst readEmbeddingsForFile = (fileKey: string): Record<string, number[]> => {\n try {\n const raw = JSON.parse(\n readFileSync(\n join(__dirname, `./embeddings/${fileKey.replace('.md', '.json')}`),\n 'utf-8'\n )\n ) as Record<string, unknown>;\n // Strip hash entries (keys ending in _hash) — only return actual embedding vectors\n return Object.fromEntries(\n Object.entries(raw).filter(\n ([key, value]) => !key.endsWith('_hash') && Array.isArray(value)\n )\n ) as Record<string, number[]>;\n } catch {\n return {};\n }\n};\n\ntype VectorStoreEl = {\n fileKey: string;\n chunkNumber: number;\n content: string;\n embedding?: number[];\n docUrl?: string;\n docName?: string;\n};\n\n/**\n * Simple in-memory vector store to hold document embeddings and their content.\n * Each entry contains:\n * - fileKey: A unique key identifying the file\n * - chunkNumber: The number of the chunk within the document\n * - content: The chunk content\n * - embedding: The numerical embedding vector for the chunk\n */\nconst vectorStore: VectorStoreEl[] = [];\n\n/*\n * Ask question AI configuration\n */\nconst MAX_RELEVANT_CHUNKS_NB: number = 15; // Maximum number of relevant chunks to attach to chatGPT context\nconst MIN_RELEVANT_CHUNKS_SIMILARITY: number = 0.42; // Minimum similarity required for a chunk to be considered relevant\n\n/*\n * Embedding model configuration\n */\nconst EMBEDDING_MODEL: OpenAI.EmbeddingModel = 'text-embedding-3-large'; // Model to use for embedding generation\nconst OVERLAP_TOKENS: number = 200; // Number of tokens to overlap between chunks\nconst MAX_CHUNK_TOKENS: number = 800; // Maximum number of tokens per chunk\nconst CHAR_BY_TOKEN: number = 4.15; // Approximate pessimistically the number of characters per token // Can use `tiktoken` or other tokenizers to calculate it more precisely\nconst MAX_CHARS: number = MAX_CHUNK_TOKENS * CHAR_BY_TOKEN;\nconst OVERLAP_CHARS: number = OVERLAP_TOKENS * CHAR_BY_TOKEN;\n\nconst skipDocEmbeddingsIndex = process.env.SKIP_DOC_EMBEDDINGS_INDEX === 'true';\n\n/**\n * Splits a given text into chunks ensuring each chunk does not exceed MAX_CHARS.\n * @param text - The input text to split.\n * @returns - Array of text chunks.\n */\nconst chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let start = 0;\n\n while (start < text.length) {\n let end = Math.min(start + MAX_CHARS, text.length);\n\n // Ensure we don't cut words in the middle (find nearest space)\n if (end < text.length) {\n const lastSpace = text.lastIndexOf(' ', end);\n if (lastSpace > start) {\n end = lastSpace;\n }\n }\n\n chunks.push(text.substring(start, end));\n\n // Move start forward correctly\n const nextStart = end - OVERLAP_CHARS;\n if (nextStart <= start) {\n // Prevent infinite loop if overlap is too large\n start = end;\n } else {\n start = nextStart;\n }\n }\n\n return chunks;\n};\n\n/**\n * Generates an embedding for a given text using OpenAI's embedding API.\n *\n * @param text - The input text to generate an embedding for\n * @returns The embedding vector as a number array\n */\nconst generateEmbedding = async (text: string): Promise<number[]> => {\n // No try/catch here. If this fails, the controller should handle it.\n const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n const response = await openaiClient.embeddings.create({\n model: EMBEDDING_MODEL,\n input: text,\n });\n\n return response.data[0].embedding;\n};\n\n/**\n * Calculates the cosine similarity between two vectors.\n * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.\n * Used to determine the similarity between chunks of text.\n *\n * @param vecA - The first vector\n * @param vecB - The second vector\n * @returns The cosine similarity score\n */\nconst cosineSimilarity = (vecA: number[], vecB: number[]): number => {\n // Calculate the dot product of the two vectors\n const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);\n\n // Calculate the magnitude (Euclidean norm) of each vector\n const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));\n const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));\n\n // Compute and return the cosine similarity\n return dotProduct / (magnitudeA * magnitudeB);\n};\n\n/**\n * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.\n * Persists per-document embeddings under `embeddings/<fileKey>.json`.\n * Handles cases where files have been updated and chunk counts have changed.\n */\nexport const loadMarkdownFiles = async (): Promise<void> => {\n // Retrieve documentation and blog posts in English locale\n const frequentQuestions = await getFrequentQuestions();\n const docs = await getDocs();\n const blogs = await getBlogs();\n\n const files = { ...docs, ...blogs, ...frequentQuestions }; // Combine docs and blogs into a single object\n\n // Iterate over each file key (identifier) in the combined files\n for await (const fileKey of Object.keys(files)) {\n // Get the metadata of the file\n const fileMetadata = getMarkdownMetadata<{\n url?: string;\n title?: string;\n slugs?: (string | number)[];\n description?: string;\n }>(files[fileKey as keyof typeof files] as string);\n\n const slugs = (fileMetadata.slugs ?? []).map(String);\n const docUrl =\n fileMetadata.url ??\n (slugs.length > 0\n ? `${process.env.WEBSITE_URL}/${slugs.join('/')}`\n : undefined);\n\n // Split the document into chunks based on headings\n const fileChunks = chunkText(\n files[fileKey as keyof typeof files] as string\n );\n\n // Read existing embeddings for this file\n const existingEmbeddings = readEmbeddingsForFile(fileKey);\n\n // Check if the number of chunks has changed for this file\n const existingChunksForFile = Object.keys(existingEmbeddings);\n const currentChunkCount = fileChunks.length;\n const previousChunkCount = existingChunksForFile.length;\n\n let shouldRegenerateFileEmbeddings = false;\n\n // If chunk count differs, we need to regenerate embeddings for this file\n if (currentChunkCount !== previousChunkCount) {\n logger.info(\n `File \"${fileKey}\" chunk count changed: ${previousChunkCount} -> ${currentChunkCount}. Regenerating embeddings.`\n );\n\n shouldRegenerateFileEmbeddings = !skipDocEmbeddingsIndex;\n }\n\n // Iterate over each chunk within the current file\n let resultForFile: Record<string, number[] | undefined> = {};\n for await (const chunkIndex of Object.keys(fileChunks)) {\n const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1\n const chunksNumber = fileChunks.length;\n\n const fileChunk = fileChunks[\n chunkIndex as keyof typeof fileChunks\n ] as string;\n\n const chunkKeyName = `chunk_${chunkNumber}`; // Unique key for the chunk within the file\n\n // Retrieve precomputed embedding if available and file hasn't changed\n const docEmbedding = !shouldRegenerateFileEmbeddings\n ? (existingEmbeddings[\n chunkKeyName as keyof typeof existingEmbeddings\n ] as number[] | undefined)\n : undefined;\n\n const embedding = docEmbedding; // Use existing embedding if available and valid\n\n // Update the file-scoped result object with the embedding\n resultForFile = { ...resultForFile, [chunkKeyName]: embedding };\n\n // Store the embedding and content in the in-memory vector store\n vectorStore.push({\n fileKey,\n chunkNumber,\n embedding,\n content: fileChunk,\n docUrl,\n docName: fileMetadata.title,\n });\n\n logger.info(`- Loaded: ${fileKey}/${chunkKeyName}/${chunksNumber}`);\n }\n }\n};\n\n// Automatically index Markdown files\nloadMarkdownFiles();\n\n/**\n * Searches the indexed documents for the most relevant chunks based on a query.\n * Utilizes cosine similarity to find the closest matching embeddings.\n *\n * @param query - The search query provided by the user\n * @returns An array of the top matching document chunks' content\n */\nexport const searchChunkReference = async (\n query: string,\n maxResults: number = MAX_RELEVANT_CHUNKS_NB,\n minSimilarity: number = MIN_RELEVANT_CHUNKS_SIMILARITY\n): Promise<VectorStoreEl[]> => {\n // Generate an embedding for the user's query\n const queryEmbedding = await generateEmbedding(query);\n\n // Calculate similarity scores between the query embedding and each document's embedding\n const selection = vectorStore\n .filter((chunk) => chunk.embedding)\n .map((chunk) => ({\n ...chunk,\n similarity: cosineSimilarity(queryEmbedding, chunk.embedding!), // Add similarity score to each doc\n }))\n .filter((chunk) => chunk.similarity > minSimilarity) // Filter out documents with low similarity scores\n .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first\n .slice(0, maxResults); // Select the top 6 most similar documents\n\n const orderedDocKeys = new Set(selection.map((chunk) => chunk.fileKey));\n\n const orderedVectorStore = vectorStore.sort((a, _b) =>\n orderedDocKeys.has(a.fileKey) ? -1 : 1\n );\n\n const results = orderedVectorStore.filter((chunk) =>\n selection.some(\n (v) => v.fileKey === chunk.fileKey && v.chunkNumber === chunk.chunkNumber\n )\n );\n\n // Return the content of the top matching documents\n return results;\n};\n\nconst CHAT_GPT_PROMPT = readFileSync(join(__dirname, './PROMPT.md'), 'utf-8');\n\n// Initial prompt configuration for the chatbot\nexport const initPrompt: ChatCompletionRequestMessage = {\n role: 'system',\n content: CHAT_GPT_PROMPT,\n};\n\nexport type AskDocQuestionResult = {\n response: string;\n relatedFiles: string[];\n};\n\nexport type AskDocQuestionOptions = {\n onMessage?: (chunk: string) => void;\n};\n\n/**\n * Handles the \"Ask a question\" endpoint in an Express.js route.\n * Processes user messages, retrieves relevant documents, and interacts with AI models to generate responses.\n *\n * @param messages - An array of chat messages from the user and assistant\n * @returns The assistant's response as a string\n */\nexport const askDocQuestion = async (\n messages: ChatCompletionRequestMessage[],\n aiConfig: AIConfig,\n options?: AskDocQuestionOptions\n): Promise<AskDocQuestionResult> => {\n // Format the user's question to keep only the relevant keywords\n const query = messages\n .filter((message) => message.role === 'user')\n .map((message) => `- ${message.content}`)\n .join('\\n');\n\n // Find relevant documents based on the user's question\n const relevantFilesReferences = await searchChunkReference(query);\n\n // Integrate the relevant documents into the initial system prompt\n const systemPrompt = initPrompt.content.replace(\n '{{relevantFilesReferences}}',\n relevantFilesReferences.length === 0\n ? 'Not relevant file found related to the question.'\n : relevantFilesReferences\n .map((doc, idx) =>\n [\n '-----',\n '---',\n `chunkId: ${idx}`,\n `docChunk: \"${doc.chunkNumber}/${doc.fileKey.length}\"`,\n `docName: \"${doc.docName}\"`,\n `docUrl: \"${doc.docUrl}\"`,\n `---`,\n doc.content,\n `-----`,\n ].join('\\n')\n )\n .join('\\n\\n') // Insert relevant docs into the prompt\n );\n\n let processedMessages: ChatCompletionRequestMessage[] = messages;\n\n if (messages.length > 8) {\n const truncatedCount = messages.length - 8;\n const placeholderMessage = {\n role: 'system',\n content: `(truncated discussion, ${truncatedCount} more messages)`,\n } as const;\n\n processedMessages = [\n ...messages.slice(0, 3),\n placeholderMessage,\n ...messages.slice(-5),\n ];\n }\n\n if (!aiConfig) {\n throw new Error('Failed to initialize AI configuration');\n }\n\n // Use the AI SDK to stream the response\n let fullResponse = '';\n const stream = streamText({\n ...aiConfig,\n system: systemPrompt,\n messages: processedMessages,\n maxOutputTokens: 500,\n });\n\n // Process the stream\n for await (const chunk of stream.textStream) {\n fullResponse += chunk;\n options?.onMessage?.(chunk);\n }\n\n // Extract unique related files\n const relatedFiles = [\n ...new Set(relevantFilesReferences.map((doc) => doc.fileKey)),\n ];\n\n // Return the assistant's response to the user\n return {\n response: fullResponse ?? 'Error: No result found',\n relatedFiles,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"askDocQuestion.mjs","names":[],"sources":["../../../../../src/utils/AI/askDocQuestion/askDocQuestion.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n type AIConfig,\n type ChatCompletionRequestMessage,\n streamText,\n} from '@intlayer/ai';\nimport { getMarkdownMetadata } from '@intlayer/core/markdown';\nimport { getBlogs, getDocs, getFrequentQuestions } from '@intlayer/docs';\nimport { logger } from '@logger';\nimport { OpenAI } from 'openai';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst readEmbeddingsForFile = (fileKey: string): Record<string, number[]> => {\n try {\n const raw = JSON.parse(\n readFileSync(\n join(__dirname, `./embeddings/${fileKey.replace('.md', '.json')}`),\n 'utf-8'\n )\n ) as Record<string, unknown>;\n // Strip hash entries (keys ending in _hash) — only return actual embedding vectors\n return Object.fromEntries(\n Object.entries(raw).filter(\n ([key, value]) => !key.endsWith('_hash') && Array.isArray(value)\n )\n ) as Record<string, number[]>;\n } catch {\n return {};\n }\n};\n\ntype VectorStoreEl = {\n fileKey: string;\n chunkNumber: number;\n content: string;\n embedding?: number[];\n docUrl?: string;\n docName?: string;\n};\n\n/**\n * Simple in-memory vector store to hold document embeddings and their content.\n * Each entry contains:\n * - fileKey: A unique key identifying the file\n * - chunkNumber: The number of the chunk within the document\n * - content: The chunk content\n * - embedding: The numerical embedding vector for the chunk\n */\nconst vectorStore: VectorStoreEl[] = [];\n\n/*\n * Ask question AI configuration\n */\nconst MAX_RELEVANT_CHUNKS_NB: number = 15; // Maximum number of relevant chunks to attach to chatGPT context\nconst MIN_RELEVANT_CHUNKS_SIMILARITY: number = 0.42; // Minimum similarity required for a chunk to be considered relevant\n\n/*\n * Embedding model configuration\n */\nconst EMBEDDING_MODEL: OpenAI.EmbeddingModel = 'text-embedding-3-large'; // Model to use for embedding generation\nconst OVERLAP_TOKENS: number = 200; // Number of tokens to overlap between chunks\nconst MAX_CHUNK_TOKENS: number = 800; // Maximum number of tokens per chunk\nconst CHAR_BY_TOKEN: number = 4.15; // Approximate pessimistically the number of characters per token // Can use `tiktoken` or other tokenizers to calculate it more precisely\nconst MAX_CHARS: number = MAX_CHUNK_TOKENS * CHAR_BY_TOKEN;\nconst OVERLAP_CHARS: number = OVERLAP_TOKENS * CHAR_BY_TOKEN;\n\nconst skipDocEmbeddingsIndex = process.env.SKIP_DOC_EMBEDDINGS_INDEX === 'true';\n\n/**\n * Splits a given text into chunks ensuring each chunk does not exceed MAX_CHARS.\n * @param text - The input text to split.\n * @returns - Array of text chunks.\n */\nconst chunkText = (text: string): string[] => {\n const chunks: string[] = [];\n let start = 0;\n\n while (start < text.length) {\n let end = Math.min(start + MAX_CHARS, text.length);\n\n // Ensure we don't cut words in the middle (find nearest space)\n if (end < text.length) {\n const lastSpace = text.lastIndexOf(' ', end);\n if (lastSpace > start) {\n end = lastSpace;\n }\n }\n\n chunks.push(text.substring(start, end));\n\n // Move start forward correctly\n const nextStart = end - OVERLAP_CHARS;\n if (nextStart <= start) {\n // Prevent infinite loop if overlap is too large\n start = end;\n } else {\n start = nextStart;\n }\n }\n\n return chunks;\n};\n\n/**\n * Generates an embedding for a given text using OpenAI's embedding API.\n *\n * @param text - The input text to generate an embedding for\n * @returns The embedding vector as a number array\n */\nconst generateEmbedding = async (text: string): Promise<number[]> => {\n // No try/catch here. If this fails, the controller should handle it.\n const openaiClient = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\n\n const response = await openaiClient.embeddings.create({\n model: EMBEDDING_MODEL,\n input: text,\n });\n\n return response.data[0].embedding;\n};\n\n/**\n * Calculates the cosine similarity between two vectors.\n * Cosine similarity measures the cosine of the angle between two vectors in an inner product space.\n * Used to determine the similarity between chunks of text.\n *\n * @param vecA - The first vector\n * @param vecB - The second vector\n * @returns The cosine similarity score\n */\nconst cosineSimilarity = (vecA: number[], vecB: number[]): number => {\n // Calculate the dot product of the two vectors\n const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);\n\n // Calculate the magnitude (Euclidean norm) of each vector\n const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));\n const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));\n\n // Compute and return the cosine similarity\n return dotProduct / (magnitudeA * magnitudeB);\n};\n\n/**\n * Indexes all Markdown documents by generating embeddings for each chunk and storing them in memory.\n * Persists per-document embeddings under `embeddings/<fileKey>.json`.\n * Handles cases where files have been updated and chunk counts have changed.\n */\nexport const loadMarkdownFiles = async (): Promise<void> => {\n // Retrieve documentation and blog posts in English locale\n const frequentQuestions = await getFrequentQuestions();\n const docs = await getDocs();\n const blogs = await getBlogs();\n\n const files = { ...docs, ...blogs, ...frequentQuestions }; // Combine docs and blogs into a single object\n\n // Iterate over each file key (identifier) in the combined files\n for await (const fileKey of Object.keys(files)) {\n // Get the metadata of the file\n const fileMetadata = getMarkdownMetadata<{\n url?: string;\n title?: string;\n slugs?: (string | number)[];\n description?: string;\n }>(files[fileKey as keyof typeof files] as string);\n\n const slugs = (fileMetadata.slugs ?? []).map(String);\n const docUrl =\n fileMetadata.url ??\n (slugs.length > 0\n ? `${process.env.WEBSITE_URL}/${slugs.join('/')}`\n : undefined);\n\n // Split the document into chunks based on headings\n const fileChunks = chunkText(\n files[fileKey as keyof typeof files] as string\n );\n\n // Read existing embeddings for this file\n const existingEmbeddings = readEmbeddingsForFile(fileKey);\n\n // Check if the number of chunks has changed for this file\n const existingChunksForFile = Object.keys(existingEmbeddings);\n const currentChunkCount = fileChunks.length;\n const previousChunkCount = existingChunksForFile.length;\n\n let shouldRegenerateFileEmbeddings = false;\n\n // If chunk count differs, we need to regenerate embeddings for this file\n if (currentChunkCount !== previousChunkCount) {\n logger.info(\n `File \"${fileKey}\" chunk count changed: ${previousChunkCount} -> ${currentChunkCount}. Regenerating embeddings.`\n );\n\n shouldRegenerateFileEmbeddings = !skipDocEmbeddingsIndex;\n }\n\n // Iterate over each chunk within the current file\n let resultForFile: Record<string, number[] | undefined> = {};\n for await (const chunkIndex of Object.keys(fileChunks)) {\n const chunkNumber = Number(chunkIndex) + 1; // Chunk number starts at 1\n const chunksNumber = fileChunks.length;\n\n const fileChunk = fileChunks[\n chunkIndex as keyof typeof fileChunks\n ] as string;\n\n const chunkKeyName = `chunk_${chunkNumber}`; // Unique key for the chunk within the file\n\n // Retrieve precomputed embedding if available and file hasn't changed\n const docEmbedding = !shouldRegenerateFileEmbeddings\n ? (existingEmbeddings[\n chunkKeyName as keyof typeof existingEmbeddings\n ] as number[] | undefined)\n : undefined;\n\n const embedding = docEmbedding; // Use existing embedding if available and valid\n\n // Update the file-scoped result object with the embedding\n resultForFile = { ...resultForFile, [chunkKeyName]: embedding };\n\n // Store the embedding and content in the in-memory vector store\n vectorStore.push({\n fileKey,\n chunkNumber,\n embedding,\n content: fileChunk,\n docUrl,\n docName: fileMetadata.title,\n });\n\n logger.info(`- Loaded: ${fileKey}/${chunkKeyName}/${chunksNumber}`);\n }\n }\n};\n\n// Automatically index Markdown files\nloadMarkdownFiles();\n\n/**\n * Searches the indexed documents for the most relevant chunks based on a query.\n * Utilizes cosine similarity to find the closest matching embeddings.\n *\n * @param query - The search query provided by the user\n * @returns An array of the top matching document chunks' content\n */\nexport const searchChunkReference = async (\n query: string,\n maxResults: number = MAX_RELEVANT_CHUNKS_NB,\n minSimilarity: number = MIN_RELEVANT_CHUNKS_SIMILARITY\n): Promise<VectorStoreEl[]> => {\n // Generate an embedding for the user's query\n const queryEmbedding = await generateEmbedding(query);\n\n // Calculate similarity scores between the query embedding and each document's embedding\n const selection = vectorStore\n .filter((chunk) => chunk.embedding)\n .map((chunk) => ({\n ...chunk,\n similarity: cosineSimilarity(queryEmbedding, chunk.embedding!), // Add similarity score to each doc\n }))\n .filter((chunk) => chunk.similarity > minSimilarity) // Filter out documents with low similarity scores\n .sort((a, b) => b.similarity - a.similarity) // Sort documents by highest similarity first\n .slice(0, maxResults); // Select the top 6 most similar documents\n\n const orderedDocKeys = new Set(selection.map((chunk) => chunk.fileKey));\n\n const orderedVectorStore = vectorStore.sort((a, _b) =>\n orderedDocKeys.has(a.fileKey) ? -1 : 1\n );\n\n const results = orderedVectorStore.filter((chunk) =>\n selection.some(\n (v) => v.fileKey === chunk.fileKey && v.chunkNumber === chunk.chunkNumber\n )\n );\n\n // Return the content of the top matching documents\n return results;\n};\n\nconst CHAT_GPT_PROMPT = readFileSync(join(__dirname, './PROMPT.md'), 'utf-8');\n\n// Initial prompt configuration for the chatbot\nexport const initPrompt: ChatCompletionRequestMessage = {\n role: 'system',\n content: CHAT_GPT_PROMPT,\n};\n\nexport type AskDocQuestionResult = {\n response: string;\n relatedFiles: string[];\n};\n\nexport type AskDocQuestionOptions = {\n onMessage?: (chunk: string) => void;\n};\n\n/**\n * Handles the \"Ask a question\" endpoint in an Express.js route.\n * Processes user messages, retrieves relevant documents, and interacts with AI models to generate responses.\n *\n * @param messages - An array of chat messages from the user and assistant\n * @returns The assistant's response as a string\n */\nexport const askDocQuestion = async (\n messages: ChatCompletionRequestMessage[],\n aiConfig: AIConfig,\n options?: AskDocQuestionOptions\n): Promise<AskDocQuestionResult> => {\n // Format the user's question to keep only the relevant keywords\n const query = messages\n .filter((message) => message.role === 'user')\n .map((message) => `- ${message.content}`)\n .join('\\n');\n\n // Find relevant documents based on the user's question\n const relevantFilesReferences = await searchChunkReference(query);\n\n // Integrate the relevant documents into the initial system prompt\n const systemPrompt = initPrompt.content.replace(\n '{{relevantFilesReferences}}',\n relevantFilesReferences.length === 0\n ? 'Not relevant file found related to the question.'\n : relevantFilesReferences\n .map((doc, idx) =>\n [\n '-----',\n '---',\n `chunkId: ${idx}`,\n `docChunk: \"${doc.chunkNumber}/${doc.fileKey.length}\"`,\n `docName: \"${doc.docName}\"`,\n `docUrl: \"${doc.docUrl}\"`,\n `---`,\n doc.content,\n `-----`,\n ].join('\\n')\n )\n .join('\\n\\n') // Insert relevant docs into the prompt\n );\n\n let processedMessages: ChatCompletionRequestMessage[] = messages;\n\n if (messages.length > 8) {\n const truncatedCount = messages.length - 8;\n const placeholderMessage = {\n role: 'system',\n content: `(truncated discussion, ${truncatedCount} more messages)`,\n } as const;\n\n processedMessages = [\n ...messages.slice(0, 3),\n placeholderMessage,\n ...messages.slice(-5),\n ];\n }\n\n if (!aiConfig) {\n throw new Error('Failed to initialize AI configuration');\n }\n\n // Use the AI SDK to stream the response\n let fullResponse = '';\n const stream = streamText({\n ...aiConfig,\n system: systemPrompt,\n messages: processedMessages,\n maxOutputTokens: 500,\n });\n\n // Process the stream\n for await (const chunk of stream.textStream) {\n fullResponse += chunk;\n options?.onMessage?.(chunk);\n }\n\n // Extract unique related files\n const relatedFiles = [\n ...new Set(relevantFilesReferences.map((doc) => doc.fileKey)),\n ];\n\n // Return the assistant's response to the user\n return {\n response: fullResponse ?? 'Error: No result found',\n relatedFiles,\n };\n};\n"],"mappings":"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"}