@intlayer/backend 8.10.0-canary.1 → 8.10.1

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 (186) hide show
  1. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +12966 -1
  2. package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/interest_of_intlayer.json +1 -14957
  3. package/dist/esm/controllers/ai.controller.mjs +10 -9
  4. package/dist/esm/controllers/ai.controller.mjs.map +1 -1
  5. package/dist/esm/controllers/audit.controller.mjs +1 -1
  6. package/dist/esm/controllers/audit.controller.mjs.map +1 -1
  7. package/dist/esm/controllers/organization.controller.mjs +17 -5
  8. package/dist/esm/controllers/organization.controller.mjs.map +1 -1
  9. package/dist/esm/controllers/project.controller.mjs +12 -5
  10. package/dist/esm/controllers/project.controller.mjs.map +1 -1
  11. package/dist/esm/controllers/showcaseProject.controller.mjs +0 -2
  12. package/dist/esm/controllers/showcaseProject.controller.mjs.map +1 -1
  13. package/dist/esm/controllers/translation.controller.mjs +0 -1
  14. package/dist/esm/controllers/translation.controller.mjs.map +1 -1
  15. package/dist/esm/controllers/user.controller.mjs +0 -6
  16. package/dist/esm/controllers/user.controller.mjs.map +1 -1
  17. package/dist/esm/schemas/account.schema.mjs +3 -2
  18. package/dist/esm/schemas/account.schema.mjs.map +1 -1
  19. package/dist/esm/{models/audit.model.mjs → schemas/audit.schema.mjs} +3 -3
  20. package/dist/esm/schemas/audit.schema.mjs.map +1 -0
  21. package/dist/esm/{models/auditJob.model.mjs → schemas/auditJob.schema.mjs} +3 -3
  22. package/dist/esm/schemas/auditJob.schema.mjs.map +1 -0
  23. package/dist/esm/{models/auditPage.model.mjs → schemas/auditPage.schema.mjs} +3 -3
  24. package/dist/esm/schemas/auditPage.schema.mjs.map +1 -0
  25. package/dist/esm/schemas/cliSessionToken.schema.mjs +3 -2
  26. package/dist/esm/schemas/cliSessionToken.schema.mjs.map +1 -1
  27. package/dist/esm/schemas/dictionary.schema.mjs +3 -2
  28. package/dist/esm/schemas/dictionary.schema.mjs.map +1 -1
  29. package/dist/esm/schemas/discussion.schema.mjs +3 -2
  30. package/dist/esm/schemas/discussion.schema.mjs.map +1 -1
  31. package/dist/esm/schemas/oAuth2.schema.mjs +3 -2
  32. package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
  33. package/dist/esm/schemas/organization.schema.mjs +3 -2
  34. package/dist/esm/schemas/organization.schema.mjs.map +1 -1
  35. package/dist/esm/schemas/project.schema.mjs +3 -2
  36. package/dist/esm/schemas/project.schema.mjs.map +1 -1
  37. package/dist/esm/schemas/session.schema.mjs +3 -2
  38. package/dist/esm/schemas/session.schema.mjs.map +1 -1
  39. package/dist/esm/schemas/showcaseProject.schema.mjs +3 -2
  40. package/dist/esm/schemas/showcaseProject.schema.mjs.map +1 -1
  41. package/dist/esm/schemas/tag.schema.mjs +3 -2
  42. package/dist/esm/schemas/tag.schema.mjs.map +1 -1
  43. package/dist/esm/schemas/user.schema.mjs +11 -2
  44. package/dist/esm/schemas/user.schema.mjs.map +1 -1
  45. package/dist/esm/services/audit/recursiveAudit.service.mjs +2 -2
  46. package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
  47. package/dist/esm/services/bitbucket.service.mjs +1 -1
  48. package/dist/esm/services/bitbucket.service.mjs.map +1 -1
  49. package/dist/esm/services/cliSessionToken.service.mjs +1 -1
  50. package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
  51. package/dist/esm/services/dictionary.service.mjs +1 -1
  52. package/dist/esm/services/dictionary.service.mjs.map +1 -1
  53. package/dist/esm/services/github.service.mjs +1 -1
  54. package/dist/esm/services/github.service.mjs.map +1 -1
  55. package/dist/esm/services/gitlab.service.mjs +1 -1
  56. package/dist/esm/services/gitlab.service.mjs.map +1 -1
  57. package/dist/esm/services/oAuth2.service.mjs +2 -2
  58. package/dist/esm/services/oAuth2.service.mjs.map +1 -1
  59. package/dist/esm/services/organization.service.mjs +1 -1
  60. package/dist/esm/services/organization.service.mjs.map +1 -1
  61. package/dist/esm/services/project.service.mjs +1 -1
  62. package/dist/esm/services/project.service.mjs.map +1 -1
  63. package/dist/esm/services/projectAccessKey.service.mjs +1 -1
  64. package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
  65. package/dist/esm/services/showcase/showcaseProject.service.mjs +1 -1
  66. package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
  67. package/dist/esm/services/tag.service.mjs +1 -1
  68. package/dist/esm/services/tag.service.mjs.map +1 -1
  69. package/dist/esm/services/user.service.mjs +1 -1
  70. package/dist/esm/services/user.service.mjs.map +1 -1
  71. package/dist/esm/types/user.types.mjs.map +1 -1
  72. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +12966 -1
  73. package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/interest_of_intlayer.json +1 -14957
  74. package/dist/esm/utils/AI/auditDictionaryField/index.mjs +9 -0
  75. package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
  76. package/dist/esm/utils/AI/getProjectAIOptions.mjs +20 -0
  77. package/dist/esm/utils/AI/getProjectAIOptions.mjs.map +1 -0
  78. package/dist/esm/utils/auth/getAuth.mjs +34 -8
  79. package/dist/esm/utils/auth/getAuth.mjs.map +1 -1
  80. package/dist/esm/utils/errors/ErrorHandler.mjs +40 -8
  81. package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
  82. package/dist/esm/utils/mapper/project.mjs +7 -1
  83. package/dist/esm/utils/mapper/project.mjs.map +1 -1
  84. package/dist/esm/utils/mongoDB/connectDB.mjs +12 -12
  85. package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
  86. package/dist/types/controllers/ai.controller.d.ts.map +1 -1
  87. package/dist/types/controllers/organization.controller.d.ts.map +1 -1
  88. package/dist/types/controllers/project.controller.d.ts.map +1 -1
  89. package/dist/types/controllers/showcaseProject.controller.d.ts.map +1 -1
  90. package/dist/types/controllers/translation.controller.d.ts.map +1 -1
  91. package/dist/types/controllers/user.controller.d.ts.map +1 -1
  92. package/dist/types/schemas/account.schema.d.ts +3 -2
  93. package/dist/types/schemas/account.schema.d.ts.map +1 -1
  94. package/dist/types/schemas/audit.schema.d.ts +64 -0
  95. package/dist/types/schemas/audit.schema.d.ts.map +1 -0
  96. package/dist/types/schemas/auditJob.schema.d.ts +122 -0
  97. package/dist/types/schemas/auditJob.schema.d.ts.map +1 -0
  98. package/dist/types/schemas/auditPage.schema.d.ts +120 -0
  99. package/dist/types/schemas/auditPage.schema.d.ts.map +1 -0
  100. package/dist/types/schemas/cliSessionToken.schema.d.ts +10 -3
  101. package/dist/types/schemas/cliSessionToken.schema.d.ts.map +1 -1
  102. package/dist/types/schemas/dictionary.schema.d.ts +21 -12
  103. package/dist/types/schemas/dictionary.schema.d.ts.map +1 -1
  104. package/dist/types/schemas/discussion.schema.d.ts +16 -11
  105. package/dist/types/schemas/discussion.schema.d.ts.map +1 -1
  106. package/dist/types/schemas/oAuth2.schema.d.ts +13 -3
  107. package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
  108. package/dist/types/schemas/organization.schema.d.ts +8 -7
  109. package/dist/types/schemas/organization.schema.d.ts.map +1 -1
  110. package/dist/types/schemas/plans.schema.d.ts +8 -8
  111. package/dist/types/schemas/project.schema.d.ts +11 -10
  112. package/dist/types/schemas/project.schema.d.ts.map +1 -1
  113. package/dist/types/schemas/session.schema.d.ts +11 -10
  114. package/dist/types/schemas/session.schema.d.ts.map +1 -1
  115. package/dist/types/schemas/showcaseProject.schema.d.ts +18 -17
  116. package/dist/types/schemas/showcaseProject.schema.d.ts.map +1 -1
  117. package/dist/types/schemas/tag.schema.d.ts +10 -9
  118. package/dist/types/schemas/tag.schema.d.ts.map +1 -1
  119. package/dist/types/schemas/user.schema.d.ts +32 -9
  120. package/dist/types/schemas/user.schema.d.ts.map +1 -1
  121. package/dist/types/services/audit/recursiveAudit.service.d.ts +2 -2
  122. package/dist/types/types/project.types.d.ts +2 -1
  123. package/dist/types/types/project.types.d.ts.map +1 -1
  124. package/dist/types/types/user.types.d.ts +2 -0
  125. package/dist/types/types/user.types.d.ts.map +1 -1
  126. package/dist/types/utils/AI/getProjectAIOptions.d.ts +15 -0
  127. package/dist/types/utils/AI/getProjectAIOptions.d.ts.map +1 -0
  128. package/dist/types/utils/auth/getAuth.d.ts.map +1 -1
  129. package/dist/types/utils/errors/ErrorHandler.d.ts +10 -6
  130. package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
  131. package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +3 -3
  132. package/dist/types/utils/mapper/project.d.ts.map +1 -1
  133. package/package.json +10 -10
  134. package/dist/esm/models/account.model.mjs +0 -9
  135. package/dist/esm/models/account.model.mjs.map +0 -1
  136. package/dist/esm/models/audit.model.mjs.map +0 -1
  137. package/dist/esm/models/auditJob.model.mjs.map +0 -1
  138. package/dist/esm/models/auditPage.model.mjs.map +0 -1
  139. package/dist/esm/models/cliSessionToken.model.mjs +0 -9
  140. package/dist/esm/models/cliSessionToken.model.mjs.map +0 -1
  141. package/dist/esm/models/dictionary.model.mjs +0 -9
  142. package/dist/esm/models/dictionary.model.mjs.map +0 -1
  143. package/dist/esm/models/discussion.model.mjs +0 -9
  144. package/dist/esm/models/discussion.model.mjs.map +0 -1
  145. package/dist/esm/models/oAuth2.model.mjs +0 -9
  146. package/dist/esm/models/oAuth2.model.mjs.map +0 -1
  147. package/dist/esm/models/organization.model.mjs +0 -9
  148. package/dist/esm/models/organization.model.mjs.map +0 -1
  149. package/dist/esm/models/project.model.mjs +0 -9
  150. package/dist/esm/models/project.model.mjs.map +0 -1
  151. package/dist/esm/models/session.model.mjs +0 -9
  152. package/dist/esm/models/session.model.mjs.map +0 -1
  153. package/dist/esm/models/showcaseProject.model.mjs +0 -9
  154. package/dist/esm/models/showcaseProject.model.mjs.map +0 -1
  155. package/dist/esm/models/tag.model.mjs +0 -9
  156. package/dist/esm/models/tag.model.mjs.map +0 -1
  157. package/dist/esm/models/user.model.mjs +0 -9
  158. package/dist/esm/models/user.model.mjs.map +0 -1
  159. package/dist/types/models/account.model.d.ts +0 -7
  160. package/dist/types/models/account.model.d.ts.map +0 -1
  161. package/dist/types/models/audit.model.d.ts +0 -18
  162. package/dist/types/models/audit.model.d.ts.map +0 -1
  163. package/dist/types/models/auditJob.model.d.ts +0 -31
  164. package/dist/types/models/auditJob.model.d.ts.map +0 -1
  165. package/dist/types/models/auditPage.model.d.ts +0 -29
  166. package/dist/types/models/auditPage.model.d.ts.map +0 -1
  167. package/dist/types/models/cliSessionToken.model.d.ts +0 -14
  168. package/dist/types/models/cliSessionToken.model.d.ts.map +0 -1
  169. package/dist/types/models/dictionary.model.d.ts +0 -16
  170. package/dist/types/models/dictionary.model.d.ts.map +0 -1
  171. package/dist/types/models/discussion.model.d.ts +0 -12
  172. package/dist/types/models/discussion.model.d.ts.map +0 -1
  173. package/dist/types/models/oAuth2.model.d.ts +0 -18
  174. package/dist/types/models/oAuth2.model.d.ts.map +0 -1
  175. package/dist/types/models/organization.model.d.ts +0 -7
  176. package/dist/types/models/organization.model.d.ts.map +0 -1
  177. package/dist/types/models/project.model.d.ts +0 -7
  178. package/dist/types/models/project.model.d.ts.map +0 -1
  179. package/dist/types/models/session.model.d.ts +0 -7
  180. package/dist/types/models/session.model.d.ts.map +0 -1
  181. package/dist/types/models/showcaseProject.model.d.ts +0 -7
  182. package/dist/types/models/showcaseProject.model.d.ts.map +0 -1
  183. package/dist/types/models/tag.model.d.ts +0 -7
  184. package/dist/types/models/tag.model.d.ts.map +0 -1
  185. package/dist/types/models/user.model.d.ts +0 -7
  186. package/dist/types/models/user.model.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"user.schema.mjs","names":[],"sources":["../../../src/schemas/user.schema.ts"],"sourcesContent":["import {\n NAMES_MAX_LENGTH,\n NAMES_MIN_LENGTH,\n} from '@utils/validation/validateUser';\nimport { Schema } from 'mongoose';\nimport validator from 'validator';\nimport type { UserSchema } from '@/types/user.types';\n\nexport const userSchema = new Schema<UserSchema>(\n {\n email: {\n type: String,\n required: true,\n unique: true,\n validate: [validator.isEmail, 'Please fill a valid email address'],\n lowercase: true,\n trim: true,\n },\n name: {\n type: String,\n maxlength: NAMES_MAX_LENGTH,\n minlength: NAMES_MIN_LENGTH,\n },\n image: {\n type: String,\n required: false,\n },\n phone: {\n type: String,\n maxlength: 20,\n },\n\n customerId: {\n type: String,\n required: false,\n },\n\n emailsList: {\n type: {\n newsLetter: {\n type: Boolean,\n default: false,\n },\n },\n required: false,\n },\n role: {\n type: String,\n enum: ['admin', 'user'],\n default: 'user',\n required: false,\n },\n lastLoginMethod: {\n type: String,\n enum: ['email', 'google', 'github', 'passkey'],\n required: false,\n },\n lang: {\n type: String,\n required: false,\n },\n dateOfBirth: {\n type: Date,\n required: false,\n },\n },\n {\n timestamps: true,\n\n toJSON: {\n virtuals: true, // keep the automatic `id` getter\n versionKey: false, // drop __v\n transform(_doc, ret: any) {\n const { _id, ...rest } = ret;\n return {\n ...rest,\n id: _id.toString(),\n };\n },\n },\n toObject: {\n virtuals: true,\n transform(_doc, ret: any) {\n const { _id, ...rest } = ret;\n return {\n ...rest,\n id: _id,\n };\n },\n },\n }\n);\n"],"mappings":";;;;;AAQA,MAAa,aAAa,IAAI,OAC5B;CACE,OAAO;EACL,MAAM;EACN,UAAU;EACV,QAAQ;EACR,UAAU,CAAC,UAAU,SAAS,mCAAmC;EACjE,WAAW;EACX,MAAM;CACR;CACA,MAAM;EACJ,MAAM;EACN;EACA;CACF;CACA,OAAO;EACL,MAAM;EACN,UAAU;CACZ;CACA,OAAO;EACL,MAAM;EACN,WAAW;CACb;CAEA,YAAY;EACV,MAAM;EACN,UAAU;CACZ;CAEA,YAAY;EACV,MAAM,EACJ,YAAY;GACV,MAAM;GACN,SAAS;EACX,EACF;EACA,UAAU;CACZ;CACA,MAAM;EACJ,MAAM;EACN,MAAM,CAAC,SAAS,MAAM;EACtB,SAAS;EACT,UAAU;CACZ;CACA,iBAAiB;EACf,MAAM;EACN,MAAM;GAAC;GAAS;GAAU;GAAU;EAAS;EAC7C,UAAU;CACZ;CACA,MAAM;EACJ,MAAM;EACN,UAAU;CACZ;CACA,aAAa;EACX,MAAM;EACN,UAAU;CACZ;AACF,GACA;CACE,YAAY;CAEZ,QAAQ;EACN,UAAU;EACV,YAAY;EACZ,UAAU,MAAM,KAAU;GACxB,MAAM,EAAE,KAAK,GAAG,SAAS;GACzB,OAAO;IACL,GAAG;IACH,IAAI,IAAI,SAAS;GACnB;EACF;CACF;CACA,UAAU;EACR,UAAU;EACV,UAAU,MAAM,KAAU;GACxB,MAAM,EAAE,KAAK,GAAG,SAAS;GACzB,OAAO;IACL,GAAG;IACH,IAAI;GACN;EACF;CACF;AACF,CACF"}
1
+ {"version":3,"file":"user.schema.mjs","names":[],"sources":["../../../src/schemas/user.schema.ts"],"sourcesContent":["import {\n NAMES_MAX_LENGTH,\n NAMES_MIN_LENGTH,\n} from '@utils/validation/validateUser';\nimport { model, Schema } from 'mongoose';\nimport validator from 'validator';\nimport type { UserModelType, UserSchema } from '@/types/user.types';\n\nexport const userSchema = new Schema<UserSchema>(\n {\n email: {\n type: String,\n required: true,\n unique: true,\n validate: [validator.isEmail, 'Please fill a valid email address'],\n lowercase: true,\n trim: true,\n },\n name: {\n type: String,\n maxlength: NAMES_MAX_LENGTH,\n minlength: NAMES_MIN_LENGTH,\n },\n image: {\n type: String,\n required: false,\n },\n phone: {\n type: String,\n maxlength: 20,\n },\n\n customerId: {\n type: String,\n required: false,\n },\n\n emailsList: {\n type: {\n newsLetter: {\n type: Boolean,\n default: false,\n },\n },\n required: false,\n },\n role: {\n type: String,\n enum: ['admin', 'user'],\n default: 'user',\n required: false,\n },\n lastLoginMethod: {\n type: String,\n enum: ['email', 'google', 'github', 'passkey'],\n required: false,\n },\n lang: {\n type: String,\n required: false,\n },\n dateOfBirth: {\n type: Date,\n required: false,\n },\n lastActiveOrganizationId: {\n type: String,\n required: false,\n },\n lastActiveProjectId: {\n type: String,\n required: false,\n },\n },\n {\n timestamps: true,\n\n toJSON: {\n virtuals: true, // keep the automatic `id` getter\n versionKey: false, // drop __v\n transform(_doc, ret: any) {\n const { _id, ...rest } = ret;\n return {\n ...rest,\n id: _id.toString(),\n };\n },\n },\n toObject: {\n virtuals: true,\n transform(_doc, ret: any) {\n const { _id, ...rest } = ret;\n return {\n ...rest,\n id: _id,\n };\n },\n },\n }\n);\n\nexport const UserModel = model<UserSchema, UserModelType>('user', userSchema);\n"],"mappings":";;;;;AAQA,MAAa,aAAa,IAAI,OAC5B;CACE,OAAO;EACL,MAAM;EACN,UAAU;EACV,QAAQ;EACR,UAAU,CAAC,UAAU,SAAS,mCAAmC;EACjE,WAAW;EACX,MAAM;CACR;CACA,MAAM;EACJ,MAAM;EACN;EACA;CACF;CACA,OAAO;EACL,MAAM;EACN,UAAU;CACZ;CACA,OAAO;EACL,MAAM;EACN,WAAW;CACb;CAEA,YAAY;EACV,MAAM;EACN,UAAU;CACZ;CAEA,YAAY;EACV,MAAM,EACJ,YAAY;GACV,MAAM;GACN,SAAS;EACX,EACF;EACA,UAAU;CACZ;CACA,MAAM;EACJ,MAAM;EACN,MAAM,CAAC,SAAS,MAAM;EACtB,SAAS;EACT,UAAU;CACZ;CACA,iBAAiB;EACf,MAAM;EACN,MAAM;GAAC;GAAS;GAAU;GAAU;EAAS;EAC7C,UAAU;CACZ;CACA,MAAM;EACJ,MAAM;EACN,UAAU;CACZ;CACA,aAAa;EACX,MAAM;EACN,UAAU;CACZ;CACA,0BAA0B;EACxB,MAAM;EACN,UAAU;CACZ;CACA,qBAAqB;EACnB,MAAM;EACN,UAAU;CACZ;AACF,GACA;CACE,YAAY;CAEZ,QAAQ;EACN,UAAU;EACV,YAAY;EACZ,UAAU,MAAM,KAAU;GACxB,MAAM,EAAE,KAAK,GAAG,SAAS;GACzB,OAAO;IACL,GAAG;IACH,IAAI,IAAI,SAAS;GACnB;EACF;CACF;CACA,UAAU;EACR,UAAU;EACV,UAAU,MAAM,KAAU;GACxB,MAAM,EAAE,KAAK,GAAG,SAAS;GACzB,OAAO;IACL,GAAG;IACH,IAAI;GACN;EACF;CACF;AACF,CACF;AAEA,MAAa,YAAY,MAAiC,QAAQ,UAAU"}
@@ -1,8 +1,8 @@
1
1
  import { logger } from "../../logger/index.mjs";
2
2
  import { mutateScore } from "./analysis/calculateScore.mjs";
3
3
  import { runSingleAudit } from "./seoAudit.service.mjs";
4
- import { AuditJobModel, AuditJobStatus } from "../../models/auditJob.model.mjs";
5
- import { AuditPageModel, AuditPageStatus } from "../../models/auditPage.model.mjs";
4
+ import { AuditJobModel, AuditJobStatus } from "../../schemas/auditJob.schema.mjs";
5
+ import { AuditPageModel, AuditPageStatus } from "../../schemas/auditPage.schema.mjs";
6
6
  import { load } from "cheerio";
7
7
 
8
8
  //#region src/services/audit/recursiveAudit.service.ts
@@ -1 +1 @@
1
- {"version":3,"file":"recursiveAudit.service.mjs","names":[],"sources":["../../../../src/services/audit/recursiveAudit.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { AuditJobModel, AuditJobStatus } from '@models/auditJob.model';\nimport { AuditPageModel, AuditPageStatus } from '@models/auditPage.model';\nimport { load } from 'cheerio';\nimport { mutateScore, type Score } from './analysis/calculateScore';\nimport { runSingleAudit } from './seoAudit.service';\n\nconst SLEEP_TIME = 30000;\nconst MAX_PAGES = 10;\n\nlet isProcessing = false;\n\n/**\n * Fetches sitemap.xml for the given URL and extracts all <loc> entries.\n * Falls back to [targetUrl] if no sitemap is found.\n */\nexport const discoverUrlsFromSitemap = async (\n targetUrl: string\n): Promise<string[]> => {\n try {\n const { origin } = new URL(targetUrl);\n const sitemapUrl = `${origin}/sitemap.xml`;\n\n const response = await fetch(sitemapUrl, {\n method: 'GET',\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO-Audit-Bot/1.0)' },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) return [targetUrl];\n\n const sitemapContent = await response.text();\n const $ = load(sitemapContent, { xmlMode: true });\n\n const urls: string[] = [];\n\n // Primary <loc> entries\n $('loc').each((_, el) => {\n const url = $(el).text().trim();\n if (url) urls.push(url);\n });\n\n // Alternate hreflang URLs from <xhtml:link rel=\"alternate\" href=\"...\">\n // Cheerio in xmlMode parses these as \"xhtml:link\" elements\n $('xhtml\\\\:link[rel=\"alternate\"], link[rel=\"alternate\"]').each((_, el) => {\n const href = $(el).attr('href')?.trim();\n if (href && href !== 'x-default') urls.push(href);\n });\n\n const uniqueUrls = [...new Set(urls)];\n return uniqueUrls.length > 0 ? uniqueUrls : [targetUrl];\n } catch {\n return [targetUrl];\n }\n};\n\nexport const startRecursiveAuditJob = async (\n targetUrl: string,\n userId?: string,\n urls?: string[]\n): Promise<string> => {\n const existingJob = await AuditJobModel.findOne({\n targetUrl: String(targetUrl),\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n });\n\n if (existingJob) {\n return (existingJob._id as any).toString();\n }\n\n const pageUrls =\n urls && urls.length > 0\n ? [...new Set(urls)].slice(0, MAX_PAGES)\n : [targetUrl];\n\n const job = await AuditJobModel.create({\n targetUrl,\n userId,\n status: AuditJobStatus.PENDING,\n totalPageCount: pageUrls.length,\n });\n\n for (const url of pageUrls) {\n await AuditPageModel.create({\n jobId: job._id,\n url,\n status: AuditPageStatus.PENDING,\n }).catch(() => {\n /* ignore duplicate key errors */\n });\n }\n\n processAuditJobs().catch((err) => logger.error(err));\n\n return (job._id as any).toString();\n};\n\nexport const cancelAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.CANCELLED,\n });\n return !!result;\n};\n\nexport const pauseAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.PAUSED,\n });\n return !!result;\n};\n\nexport const resumeAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.RUNNING,\n });\n if (!result) return false;\n processAuditJobs().catch((err) => logger.error(err));\n return true;\n};\n\nexport const processAuditJobs = async (): Promise<void> => {\n if (isProcessing) return;\n isProcessing = true;\n\n try {\n while (true) {\n const job = await AuditJobModel.findOne({\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n }).sort({ createdAt: 1 });\n\n if (!job) break;\n\n if (job.status === AuditJobStatus.PENDING) {\n job.status = AuditJobStatus.RUNNING;\n await job.save();\n }\n\n // Re-fetch to detect external cancellation / pause between pages\n const freshJob = await AuditJobModel.findById(job._id);\n if (\n !freshJob ||\n freshJob.status === AuditJobStatus.CANCELLED ||\n freshJob.status === AuditJobStatus.PAUSED\n ) {\n logger.info(\n `Job ${job._id} is ${freshJob?.status ?? 'missing'} — stopping processor`\n );\n break;\n }\n\n const pendingPage = await AuditPageModel.findOne({\n jobId: job._id,\n status: AuditPageStatus.PENDING,\n });\n\n if (!pendingPage) {\n const hasMorePages = await AuditPageModel.exists({\n jobId: job._id,\n status: { $in: [AuditPageStatus.PENDING, AuditPageStatus.RUNNING] },\n });\n\n if (!hasMorePages) {\n job.status = AuditJobStatus.COMPLETED;\n job.progress = 100;\n await job.save();\n }\n break;\n }\n\n pendingPage.status = AuditPageStatus.RUNNING;\n await pendingPage.save();\n\n try {\n const { events } = await runSingleAudit(pendingPage.url, () => {});\n\n // Compute score the same way the single-page SSE controller does\n let score: Score = { score: 0, totalScore: 0 };\n for (const event of events) {\n score = mutateScore(score, event);\n }\n\n pendingPage.status = AuditPageStatus.COMPLETED;\n pendingPage.results = events;\n pendingPage.score = Math.round(\n score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0\n );\n await pendingPage.save();\n\n const totalPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n });\n const completedPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n status: AuditPageStatus.COMPLETED,\n });\n\n job.totalPageCount = totalPages;\n job.completedPageCount = completedPages;\n job.progress = Math.round((completedPages / totalPages) * 100);\n await job.save();\n } catch (err) {\n logger.error(`Failed to audit page ${pendingPage.url}:`, err);\n pendingPage.status = AuditPageStatus.FAILED;\n pendingPage.error = String(err);\n await pendingPage.save();\n }\n\n await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME));\n }\n } finally {\n isProcessing = false;\n }\n};\n\nexport const getAuditJobStatus = async (jobId: string) => {\n const job = await AuditJobModel.findById(jobId);\n if (!job) return null;\n\n const pages = await AuditPageModel.find({ jobId }).select(\n 'url status score error results'\n );\n\n return { job, pages };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"recursiveAudit.service.mjs","names":[],"sources":["../../../../src/services/audit/recursiveAudit.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { AuditJobModel, AuditJobStatus } from '@schemas/auditJob.schema';\nimport { AuditPageModel, AuditPageStatus } from '@schemas/auditPage.schema';\nimport { load } from 'cheerio';\nimport { mutateScore, type Score } from './analysis/calculateScore';\nimport { runSingleAudit } from './seoAudit.service';\n\nconst SLEEP_TIME = 30000;\nconst MAX_PAGES = 10;\n\nlet isProcessing = false;\n\n/**\n * Fetches sitemap.xml for the given URL and extracts all <loc> entries.\n * Falls back to [targetUrl] if no sitemap is found.\n */\nexport const discoverUrlsFromSitemap = async (\n targetUrl: string\n): Promise<string[]> => {\n try {\n const { origin } = new URL(targetUrl);\n const sitemapUrl = `${origin}/sitemap.xml`;\n\n const response = await fetch(sitemapUrl, {\n method: 'GET',\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO-Audit-Bot/1.0)' },\n signal: AbortSignal.timeout(10000),\n });\n\n if (!response.ok) return [targetUrl];\n\n const sitemapContent = await response.text();\n const $ = load(sitemapContent, { xmlMode: true });\n\n const urls: string[] = [];\n\n // Primary <loc> entries\n $('loc').each((_, el) => {\n const url = $(el).text().trim();\n if (url) urls.push(url);\n });\n\n // Alternate hreflang URLs from <xhtml:link rel=\"alternate\" href=\"...\">\n // Cheerio in xmlMode parses these as \"xhtml:link\" elements\n $('xhtml\\\\:link[rel=\"alternate\"], link[rel=\"alternate\"]').each((_, el) => {\n const href = $(el).attr('href')?.trim();\n if (href && href !== 'x-default') urls.push(href);\n });\n\n const uniqueUrls = [...new Set(urls)];\n return uniqueUrls.length > 0 ? uniqueUrls : [targetUrl];\n } catch {\n return [targetUrl];\n }\n};\n\nexport const startRecursiveAuditJob = async (\n targetUrl: string,\n userId?: string,\n urls?: string[]\n): Promise<string> => {\n const existingJob = await AuditJobModel.findOne({\n targetUrl: String(targetUrl),\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n });\n\n if (existingJob) {\n return (existingJob._id as any).toString();\n }\n\n const pageUrls =\n urls && urls.length > 0\n ? [...new Set(urls)].slice(0, MAX_PAGES)\n : [targetUrl];\n\n const job = await AuditJobModel.create({\n targetUrl,\n userId,\n status: AuditJobStatus.PENDING,\n totalPageCount: pageUrls.length,\n });\n\n for (const url of pageUrls) {\n await AuditPageModel.create({\n jobId: job._id,\n url,\n status: AuditPageStatus.PENDING,\n }).catch(() => {\n /* ignore duplicate key errors */\n });\n }\n\n processAuditJobs().catch((err) => logger.error(err));\n\n return (job._id as any).toString();\n};\n\nexport const cancelAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.CANCELLED,\n });\n return !!result;\n};\n\nexport const pauseAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.PAUSED,\n });\n return !!result;\n};\n\nexport const resumeAuditJob = async (jobId: string): Promise<boolean> => {\n const result = await AuditJobModel.findByIdAndUpdate(jobId, {\n status: AuditJobStatus.RUNNING,\n });\n if (!result) return false;\n processAuditJobs().catch((err) => logger.error(err));\n return true;\n};\n\nexport const processAuditJobs = async (): Promise<void> => {\n if (isProcessing) return;\n isProcessing = true;\n\n try {\n while (true) {\n const job = await AuditJobModel.findOne({\n status: { $in: [AuditJobStatus.PENDING, AuditJobStatus.RUNNING] },\n }).sort({ createdAt: 1 });\n\n if (!job) break;\n\n if (job.status === AuditJobStatus.PENDING) {\n job.status = AuditJobStatus.RUNNING;\n await job.save();\n }\n\n // Re-fetch to detect external cancellation / pause between pages\n const freshJob = await AuditJobModel.findById(job._id);\n if (\n !freshJob ||\n freshJob.status === AuditJobStatus.CANCELLED ||\n freshJob.status === AuditJobStatus.PAUSED\n ) {\n logger.info(\n `Job ${job._id} is ${freshJob?.status ?? 'missing'} — stopping processor`\n );\n break;\n }\n\n const pendingPage = await AuditPageModel.findOne({\n jobId: job._id,\n status: AuditPageStatus.PENDING,\n });\n\n if (!pendingPage) {\n const hasMorePages = await AuditPageModel.exists({\n jobId: job._id,\n status: { $in: [AuditPageStatus.PENDING, AuditPageStatus.RUNNING] },\n });\n\n if (!hasMorePages) {\n job.status = AuditJobStatus.COMPLETED;\n job.progress = 100;\n await job.save();\n }\n break;\n }\n\n pendingPage.status = AuditPageStatus.RUNNING;\n await pendingPage.save();\n\n try {\n const { events } = await runSingleAudit(pendingPage.url, () => {});\n\n // Compute score the same way the single-page SSE controller does\n let score: Score = { score: 0, totalScore: 0 };\n for (const event of events) {\n score = mutateScore(score, event);\n }\n\n pendingPage.status = AuditPageStatus.COMPLETED;\n pendingPage.results = events;\n pendingPage.score = Math.round(\n score.totalScore > 0 ? (score.score / score.totalScore) * 100 : 0\n );\n await pendingPage.save();\n\n const totalPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n });\n const completedPages = await AuditPageModel.countDocuments({\n jobId: job._id,\n status: AuditPageStatus.COMPLETED,\n });\n\n job.totalPageCount = totalPages;\n job.completedPageCount = completedPages;\n job.progress = Math.round((completedPages / totalPages) * 100);\n await job.save();\n } catch (err) {\n logger.error(`Failed to audit page ${pendingPage.url}:`, err);\n pendingPage.status = AuditPageStatus.FAILED;\n pendingPage.error = String(err);\n await pendingPage.save();\n }\n\n await new Promise((resolve) => setTimeout(resolve, SLEEP_TIME));\n }\n } finally {\n isProcessing = false;\n }\n};\n\nexport const getAuditJobStatus = async (jobId: string) => {\n const job = await AuditJobModel.findById(jobId);\n if (!job) return null;\n\n const pages = await AuditPageModel.find({ jobId }).select(\n 'url status score error results'\n );\n\n return { job, pages };\n};\n"],"mappings":";;;;;;;;AAOA,MAAM,aAAa;AACnB,MAAM,YAAY;AAElB,IAAI,eAAe;;;;;AAMnB,MAAa,0BAA0B,OACrC,cACsB;CACtB,IAAI;EACF,MAAM,EAAE,WAAW,IAAI,IAAI,SAAS;EACpC,MAAM,aAAa,GAAG,OAAO;EAE7B,MAAM,WAAW,MAAM,MAAM,YAAY;GACvC,QAAQ;GACR,SAAS,EAAE,cAAc,8CAA8C;GACvE,QAAQ,YAAY,QAAQ,GAAK;EACnC,CAAC;EAED,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,SAAS;EAGnC,MAAM,IAAI,KAAK,MADc,SAAS,KAAK,GACZ,EAAE,SAAS,KAAK,CAAC;EAEhD,MAAM,OAAiB,CAAC;EAGxB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO;GACvB,MAAM,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK;GAC9B,IAAI,KAAK,KAAK,KAAK,GAAG;EACxB,CAAC;EAID,EAAE,0DAAsD,EAAE,MAAM,GAAG,OAAO;GACxE,MAAM,OAAO,EAAE,EAAE,EAAE,KAAK,MAAM,GAAG,KAAK;GACtC,IAAI,QAAQ,SAAS,aAAa,KAAK,KAAK,IAAI;EAClD,CAAC;EAED,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;EACpC,OAAO,WAAW,SAAS,IAAI,aAAa,CAAC,SAAS;CACxD,QAAQ;EACN,OAAO,CAAC,SAAS;CACnB;AACF;AAEA,MAAa,yBAAyB,OACpC,WACA,QACA,SACoB;CACpB,MAAM,cAAc,MAAM,cAAc,QAAQ;EAC9C,WAAW,OAAO,SAAS;EAC3B,QAAQ,EAAE,KAAK,qBAA+C,EAAE;CAClE,CAAC;CAED,IAAI,aACF,OAAQ,YAAY,IAAY,SAAS;CAG3C,MAAM,WACJ,QAAQ,KAAK,SAAS,IAClB,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,IACrC,CAAC,SAAS;CAEhB,MAAM,MAAM,MAAM,cAAc,OAAO;EACrC;EACA;EACA;EACA,gBAAgB,SAAS;CAC3B,CAAC;CAED,KAAK,MAAM,OAAO,UAChB,MAAM,eAAe,OAAO;EAC1B,OAAO,IAAI;EACX;EACA;CACF,CAAC,EAAE,YAAY,CAEf,CAAC;CAGH,iBAAiB,EAAE,OAAO,QAAQ,OAAO,MAAM,GAAG,CAAC;CAEnD,OAAQ,IAAI,IAAY,SAAS;AACnC;AAEA,MAAa,iBAAiB,OAAO,UAAoC;CAIvE,OAAO,CAAC,CAAC,MAHY,cAAc,kBAAkB,OAAO,EAC1D,oBACF,CAAC;AAEH;AAEA,MAAa,gBAAgB,OAAO,UAAoC;CAItE,OAAO,CAAC,CAAC,MAHY,cAAc,kBAAkB,OAAO,EAC1D,iBACF,CAAC;AAEH;AAEA,MAAa,iBAAiB,OAAO,UAAoC;CAIvE,IAAI,CAAC,MAHgB,cAAc,kBAAkB,OAAO,EAC1D,kBACF,CAAC,GACY,OAAO;CACpB,iBAAiB,EAAE,OAAO,QAAQ,OAAO,MAAM,GAAG,CAAC;CACnD,OAAO;AACT;AAEA,MAAa,mBAAmB,YAA2B;CACzD,IAAI,cAAc;CAClB,eAAe;CAEf,IAAI;EACF,OAAO,MAAM;GACX,MAAM,MAAM,MAAM,cAAc,QAAQ,EACtC,QAAQ,EAAE,KAAK,qBAA+C,EAAE,EAClE,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC;GAExB,IAAI,CAAC,KAAK;GAEV,IAAI,IAAI,sBAAmC;IACzC,IAAI;IACJ,MAAM,IAAI,KAAK;GACjB;GAGA,MAAM,WAAW,MAAM,cAAc,SAAS,IAAI,GAAG;GACrD,IACE,CAAC,YACD,SAAS,0BACT,SAAS,qBACT;IACA,OAAO,KACL,OAAO,IAAI,IAAI,MAAM,UAAU,UAAU,UAAU,sBACrD;IACA;GACF;GAEA,MAAM,cAAc,MAAM,eAAe,QAAQ;IAC/C,OAAO,IAAI;IACX;GACF,CAAC;GAED,IAAI,CAAC,aAAa;IAMhB,IAAI,CAAC,MALsB,eAAe,OAAO;KAC/C,OAAO,IAAI;KACX,QAAQ,EAAE,KAAK,qBAAiD,EAAE;IACpE,CAAC,GAEkB;KACjB,IAAI;KACJ,IAAI,WAAW;KACf,MAAM,IAAI,KAAK;IACjB;IACA;GACF;GAEA,YAAY;GACZ,MAAM,YAAY,KAAK;GAEvB,IAAI;IACF,MAAM,EAAE,WAAW,MAAM,eAAe,YAAY,WAAW,CAAC,CAAC;IAGjE,IAAI,QAAe;KAAE,OAAO;KAAG,YAAY;IAAE;IAC7C,KAAK,MAAM,SAAS,QAClB,QAAQ,YAAY,OAAO,KAAK;IAGlC,YAAY;IACZ,YAAY,UAAU;IACtB,YAAY,QAAQ,KAAK,MACvB,MAAM,aAAa,IAAK,MAAM,QAAQ,MAAM,aAAc,MAAM,CAClE;IACA,MAAM,YAAY,KAAK;IAEvB,MAAM,aAAa,MAAM,eAAe,eAAe,EACrD,OAAO,IAAI,IACb,CAAC;IACD,MAAM,iBAAiB,MAAM,eAAe,eAAe;KACzD,OAAO,IAAI;KACX;IACF,CAAC;IAED,IAAI,iBAAiB;IACrB,IAAI,qBAAqB;IACzB,IAAI,WAAW,KAAK,MAAO,iBAAiB,aAAc,GAAG;IAC7D,MAAM,IAAI,KAAK;GACjB,SAAS,KAAK;IACZ,OAAO,MAAM,wBAAwB,YAAY,IAAI,IAAI,GAAG;IAC5D,YAAY;IACZ,YAAY,QAAQ,OAAO,GAAG;IAC9B,MAAM,YAAY,KAAK;GACzB;GAEA,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,UAAU,CAAC;EAChE;CACF,UAAU;EACR,eAAe;CACjB;AACF;AAEA,MAAa,oBAAoB,OAAO,UAAkB;CACxD,MAAM,MAAM,MAAM,cAAc,SAAS,KAAK;CAC9C,IAAI,CAAC,KAAK,OAAO;CAMjB,OAAO;EAAE;EAAK,aAJM,eAAe,KAAK,EAAE,MAAM,CAAC,EAAE,OACjD,gCACF;CAEoB;AACtB"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../logger/index.mjs";
2
- import { AccountModel } from "../models/account.model.mjs";
2
+ import { AccountModel } from "../schemas/account.schema.mjs";
3
3
  import { configurationFilesCandidates } from "@intlayer/config/node";
4
4
 
5
5
  //#region src/services/bitbucket.service.ts
@@ -1 +1 @@
1
- {"version":3,"file":"bitbucket.service.mjs","names":[],"sources":["../../../src/services/bitbucket.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport { AccountModel } from '@models/account.model';\n\nconst BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0';\nconst BITBUCKET_AUTH_URL = 'https://bitbucket.org/site/oauth2';\n\nexport type BitbucketRepository = {\n uuid: string;\n name: string;\n full_name: string;\n slug: string;\n mainbranch?: {\n name: string;\n type: string;\n };\n links: {\n html: {\n href: string;\n };\n };\n workspace: {\n slug: string;\n name: string;\n uuid: string;\n };\n owner: {\n display_name: string;\n username?: string;\n uuid: string;\n };\n updated_on: string;\n is_private: boolean;\n};\n\nexport type BitbucketTreeItem = {\n path: string;\n type: 'commit_file' | 'commit_directory';\n size?: number;\n};\n\n/**\n * Get Bitbucket (Atlassian) authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (_redirectUri: string): string => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('Bitbucket/Atlassian Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n response_type: 'code',\n state: 'bitbucket_oauth',\n });\n\n return `${BITBUCKET_AUTH_URL}/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange Bitbucket authorization code for access token\n */\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n const clientSecret = process.env.ATLASSIAN_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('Bitbucket OAuth credentials are not configured');\n }\n\n try {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\n 'base64'\n );\n\n const response = await fetch(`${BITBUCKET_AUTH_URL}/access_token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${credentials}`,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Bitbucket token exchange failed: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`Bitbucket token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging Bitbucket code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's Bitbucket repositories\n */\nexport const getUserRepositories = async (\n accessToken: string\n): Promise<BitbucketRepository[]> => {\n try {\n // First, get the current user to find their workspaces\n const userResponse = await fetch(`${BITBUCKET_API_URL}/user`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n });\n\n if (!userResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket user: ${userResponse.statusText}`\n );\n }\n\n // Get repositories the user has access to\n const reposResponse = await fetch(\n `${BITBUCKET_API_URL}/repositories?role=member&sort=-updated_on&pagelen=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!reposResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket repositories: ${reposResponse.statusText}`\n );\n }\n\n const data = await reposResponse.json();\n return data.values || [];\n } catch (error) {\n logger.error('Error fetching Bitbucket repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a Bitbucket repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n // Use Bitbucket's src API to list files recursively\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/?max_depth=10&pagelen=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n const items: BitbucketTreeItem[] = data.values || [];\n\n // Filter files that match the configuration candidates\n const foundFiles = items\n .filter((item) => {\n if (item.type !== 'commit_file') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on Bitbucket:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from Bitbucket and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(path)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching Bitbucket file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get Bitbucket access token from user's linked account (Atlassian)\n */\nexport const getBitbucketTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n // Try with 'atlassian' provider ID (as it's linked through Better Auth's atlassian provider)\n const account = await AccountModel.findOne({\n userId,\n providerId: 'atlassian',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving Bitbucket token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a Bitbucket pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n filename: string = 'bitbucket-pipelines.yml',\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(filename)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a Bitbucket pipeline file\n * Note: Bitbucket API doesn't support direct file creation via API v2.0\n * This function returns false for allowAutoPush, requiring manual installation\n */\nexport const createPipelineFile = async (\n _accessToken: string,\n _workspace: string,\n _repoSlug: string,\n _filename: string = 'bitbucket-pipelines.yml',\n _content: string,\n _branch: string = 'main',\n _message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n // Bitbucket API v2.0 doesn't support direct file creation/update\n // Users need to manually add the file or use the web interface\n // We'll throw an error indicating manual installation is required\n throw new Error(\n 'Bitbucket API does not support automatic file creation. Please manually add the pipeline file to your repository.'\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"bitbucket.service.mjs","names":[],"sources":["../../../src/services/bitbucket.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport { AccountModel } from '@schemas/account.schema';\n\nconst BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0';\nconst BITBUCKET_AUTH_URL = 'https://bitbucket.org/site/oauth2';\n\nexport type BitbucketRepository = {\n uuid: string;\n name: string;\n full_name: string;\n slug: string;\n mainbranch?: {\n name: string;\n type: string;\n };\n links: {\n html: {\n href: string;\n };\n };\n workspace: {\n slug: string;\n name: string;\n uuid: string;\n };\n owner: {\n display_name: string;\n username?: string;\n uuid: string;\n };\n updated_on: string;\n is_private: boolean;\n};\n\nexport type BitbucketTreeItem = {\n path: string;\n type: 'commit_file' | 'commit_directory';\n size?: number;\n};\n\n/**\n * Get Bitbucket (Atlassian) authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (_redirectUri: string): string => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('Bitbucket/Atlassian Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n response_type: 'code',\n state: 'bitbucket_oauth',\n });\n\n return `${BITBUCKET_AUTH_URL}/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange Bitbucket authorization code for access token\n */\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.ATLASSIAN_CLIENT_ID;\n const clientSecret = process.env.ATLASSIAN_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('Bitbucket OAuth credentials are not configured');\n }\n\n try {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\n 'base64'\n );\n\n const response = await fetch(`${BITBUCKET_AUTH_URL}/access_token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Authorization: `Basic ${credentials}`,\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n }),\n });\n\n if (!response.ok) {\n throw new Error(\n `Bitbucket token exchange failed: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`Bitbucket token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging Bitbucket code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's Bitbucket repositories\n */\nexport const getUserRepositories = async (\n accessToken: string\n): Promise<BitbucketRepository[]> => {\n try {\n // First, get the current user to find their workspaces\n const userResponse = await fetch(`${BITBUCKET_API_URL}/user`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n });\n\n if (!userResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket user: ${userResponse.statusText}`\n );\n }\n\n // Get repositories the user has access to\n const reposResponse = await fetch(\n `${BITBUCKET_API_URL}/repositories?role=member&sort=-updated_on&pagelen=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!reposResponse.ok) {\n throw new Error(\n `Failed to fetch Bitbucket repositories: ${reposResponse.statusText}`\n );\n }\n\n const data = await reposResponse.json();\n return data.values || [];\n } catch (error) {\n logger.error('Error fetching Bitbucket repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a Bitbucket repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n // Use Bitbucket's src API to list files recursively\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/?max_depth=10&pagelen=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const data = await response.json();\n const items: BitbucketTreeItem[] = data.values || [];\n\n // Filter files that match the configuration candidates\n const foundFiles = items\n .filter((item) => {\n if (item.type !== 'commit_file') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on Bitbucket:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from Bitbucket and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(path)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching Bitbucket file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get Bitbucket access token from user's linked account (Atlassian)\n */\nexport const getBitbucketTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n // Try with 'atlassian' provider ID (as it's linked through Better Auth's atlassian provider)\n const account = await AccountModel.findOne({\n userId,\n providerId: 'atlassian',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving Bitbucket token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a Bitbucket pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n workspace: string,\n repoSlug: string,\n filename: string = 'bitbucket-pipelines.yml',\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const response = await fetch(\n `${BITBUCKET_API_URL}/repositories/${workspace}/${repoSlug}/src/${encodeURIComponent(branch)}/${encodeURIComponent(filename)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a Bitbucket pipeline file\n * Note: Bitbucket API doesn't support direct file creation via API v2.0\n * This function returns false for allowAutoPush, requiring manual installation\n */\nexport const createPipelineFile = async (\n _accessToken: string,\n _workspace: string,\n _repoSlug: string,\n _filename: string = 'bitbucket-pipelines.yml',\n _content: string,\n _branch: string = 'main',\n _message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n // Bitbucket API v2.0 doesn't support direct file creation/update\n // Users need to manually add the file or use the web interface\n // We'll throw an error indicating manual installation is required\n throw new Error(\n 'Bitbucket API does not support automatic file creation. Please manually add the pipeline file to your repository.'\n );\n};\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import { getUserById } from "./user.service.mjs";
5
5
  import { mapUserToAPI } from "../utils/mapper/user.mjs";
6
6
  import { mapOrganizationToAPI } from "../utils/mapper/organization.mjs";
7
7
  import { mapProjectToAPI } from "../utils/mapper/project.mjs";
8
- import { CliSessionTokenModel } from "../models/cliSessionToken.model.mjs";
8
+ import { CliSessionTokenModel } from "../schemas/cliSessionToken.schema.mjs";
9
9
  import { randomBytes } from "node:crypto";
10
10
 
11
11
  //#region src/services/cliSessionToken.service.ts
@@ -1 +1 @@
1
- {"version":3,"file":"cliSessionToken.service.mjs","names":[],"sources":["../../../src/services/cliSessionToken.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { CliSessionTokenModel } from '@models/cliSessionToken.model';\nimport { getOrganizationById } from '@services/organization.service';\nimport { getProjectById } from '@services/project.service';\nimport { getUserById } from '@services/user.service';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport type { Types } from 'mongoose';\nimport type { SessionContext } from '@/types/session.types';\n\nexport const CLI_SESSION_TOKEN_PREFIX = 'clisession_';\nconst CLI_SESSION_EXPIRES_MS = 2 * 60 * 60 * 1000; // 2 hours\n\nexport const isCliSessionToken = (token: string): boolean =>\n token.startsWith(CLI_SESSION_TOKEN_PREFIX);\n\nexport const createCliSessionToken = async (\n userId: string | Types.ObjectId,\n organizationId: string,\n projectId: string\n): Promise<{ token: string; expiresAt: Date }> => {\n const token = CLI_SESSION_TOKEN_PREFIX + randomBytes(32).toString('hex');\n const expiresAt = new Date(Date.now() + CLI_SESSION_EXPIRES_MS);\n\n await CliSessionTokenModel.create({\n token,\n userId,\n organizationId,\n projectId,\n expiresAt,\n });\n\n return { token, expiresAt };\n};\n\nexport const getCliSessionTokenContext = async (\n token: string\n): Promise<SessionContext> => {\n if (!isCliSessionToken(token)) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const stored = await CliSessionTokenModel.findOne({ token: String(token) });\n\n if (!stored) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n if (new Date() > stored.expiresAt) {\n await CliSessionTokenModel.deleteOne({ token: String(token) });\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n const [user, project, organization] = await Promise.all([\n getUserById(String(stored.userId)),\n getProjectById(stored.projectId),\n getOrganizationById(stored.organizationId),\n ]);\n\n if (!user || !project || !organization) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n return {\n user: mapUserToAPI(user),\n project: mapProjectToAPI(project),\n organization: mapOrganizationToAPI(organization),\n authType: 'session',\n };\n};\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,2BAA2B;AACxC,MAAM,yBAAyB,OAAc;AAE7C,MAAa,qBAAqB,UAChC,MAAM,WAAW,wBAAwB;AAE3C,MAAa,wBAAwB,OACnC,QACA,gBACA,cACgD;CAChD,MAAM,QAAQ,2BAA2B,YAAY,EAAE,EAAE,SAAS,KAAK;CACvE,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,sBAAsB;CAE9D,MAAM,qBAAqB,OAAO;EAChC;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EAAE;EAAO;CAAU;AAC5B;AAEA,MAAa,4BAA4B,OACvC,UAC4B;CAC5B,IAAI,CAAC,kBAAkB,KAAK,GAC1B,MAAM,IAAI,aAAa,sBAAsB;CAG/C,MAAM,SAAS,MAAM,qBAAqB,QAAQ,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;CAE1E,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sBAAsB;CAG/C,oBAAI,IAAI,KAAK,IAAI,OAAO,WAAW;EACjC,MAAM,qBAAqB,UAAU,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;EAC7D,MAAM,IAAI,aAAa,sBAAsB;CAC/C;CAEA,MAAM,CAAC,MAAM,SAAS,gBAAgB,MAAM,QAAQ,IAAI;EACtD,YAAY,OAAO,OAAO,MAAM,CAAC;EACjC,eAAe,OAAO,SAAS;EAC/B,oBAAoB,OAAO,cAAc;CAC3C,CAAC;CAED,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,cACxB,MAAM,IAAI,aAAa,sBAAsB;CAG/C,OAAO;EACL,MAAM,aAAa,IAAI;EACvB,SAAS,gBAAgB,OAAO;EAChC,cAAc,qBAAqB,YAAY;EAC/C,UAAU;CACZ;AACF"}
1
+ {"version":3,"file":"cliSessionToken.service.mjs","names":[],"sources":["../../../src/services/cliSessionToken.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { CliSessionTokenModel } from '@schemas/cliSessionToken.schema';\nimport { getOrganizationById } from '@services/organization.service';\nimport { getProjectById } from '@services/project.service';\nimport { getUserById } from '@services/user.service';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport type { Types } from 'mongoose';\nimport type { SessionContext } from '@/types/session.types';\n\nexport const CLI_SESSION_TOKEN_PREFIX = 'clisession_';\nconst CLI_SESSION_EXPIRES_MS = 2 * 60 * 60 * 1000; // 2 hours\n\nexport const isCliSessionToken = (token: string): boolean =>\n token.startsWith(CLI_SESSION_TOKEN_PREFIX);\n\nexport const createCliSessionToken = async (\n userId: string | Types.ObjectId,\n organizationId: string,\n projectId: string\n): Promise<{ token: string; expiresAt: Date }> => {\n const token = CLI_SESSION_TOKEN_PREFIX + randomBytes(32).toString('hex');\n const expiresAt = new Date(Date.now() + CLI_SESSION_EXPIRES_MS);\n\n await CliSessionTokenModel.create({\n token,\n userId,\n organizationId,\n projectId,\n expiresAt,\n });\n\n return { token, expiresAt };\n};\n\nexport const getCliSessionTokenContext = async (\n token: string\n): Promise<SessionContext> => {\n if (!isCliSessionToken(token)) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const stored = await CliSessionTokenModel.findOne({ token: String(token) });\n\n if (!stored) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n if (new Date() > stored.expiresAt) {\n await CliSessionTokenModel.deleteOne({ token: String(token) });\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n const [user, project, organization] = await Promise.all([\n getUserById(String(stored.userId)),\n getProjectById(stored.projectId),\n getOrganizationById(stored.organizationId),\n ]);\n\n if (!user || !project || !organization) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n return {\n user: mapUserToAPI(user),\n project: mapProjectToAPI(project),\n organization: mapOrganizationToAPI(organization),\n authType: 'session',\n };\n};\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,2BAA2B;AACxC,MAAM,yBAAyB,OAAc;AAE7C,MAAa,qBAAqB,UAChC,MAAM,WAAW,wBAAwB;AAE3C,MAAa,wBAAwB,OACnC,QACA,gBACA,cACgD;CAChD,MAAM,QAAQ,2BAA2B,YAAY,EAAE,EAAE,SAAS,KAAK;CACvE,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,IAAI,sBAAsB;CAE9D,MAAM,qBAAqB,OAAO;EAChC;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,OAAO;EAAE;EAAO;CAAU;AAC5B;AAEA,MAAa,4BAA4B,OACvC,UAC4B;CAC5B,IAAI,CAAC,kBAAkB,KAAK,GAC1B,MAAM,IAAI,aAAa,sBAAsB;CAG/C,MAAM,SAAS,MAAM,qBAAqB,QAAQ,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;CAE1E,IAAI,CAAC,QACH,MAAM,IAAI,aAAa,sBAAsB;CAG/C,oBAAI,IAAI,KAAK,IAAI,OAAO,WAAW;EACjC,MAAM,qBAAqB,UAAU,EAAE,OAAO,OAAO,KAAK,EAAE,CAAC;EAC7D,MAAM,IAAI,aAAa,sBAAsB;CAC/C;CAEA,MAAM,CAAC,MAAM,SAAS,gBAAgB,MAAM,QAAQ,IAAI;EACtD,YAAY,OAAO,OAAO,MAAM,CAAC;EACjC,eAAe,OAAO,SAAS;EAC/B,oBAAoB,OAAO,cAAc;CAC3C,CAAC;CAED,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,cACxB,MAAM,IAAI,aAAa,sBAAsB;CAG/C,OAAO;EACL,MAAM,aAAa,IAAI;EACvB,SAAS,gBAAgB,OAAO;EAChC,cAAc,qBAAqB,YAAY;EAC/C,UAAU;CACZ;AACF"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../logger/index.mjs";
2
- import { DictionaryModel } from "../models/dictionary.model.mjs";
2
+ import { DictionaryModel } from "../schemas/dictionary.schema.mjs";
3
3
  import { getDemoDictionaries } from "../utils/demoDictionaries.mjs";
4
4
  import { ensureMongoDocumentToObject } from "../utils/ensureMongoDocumentToObject.mjs";
5
5
  import { GenericError } from "../utils/errors/ErrorsClass.mjs";
@@ -1 +1 @@
1
- {"version":3,"file":"dictionary.service.mjs","names":[],"sources":["../../../src/services/dictionary.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { DictionaryModel } from '@models/dictionary.model';\nimport { getDemoDictionaries } from '@utils/demoDictionaries';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { DictionaryFilters } from '@utils/filtersAndPagination/getDictionaryFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type DictionaryFields,\n validateDictionary,\n} from '@utils/validation/validateDictionary';\nimport { Types } from 'mongoose';\nimport type {\n Dictionary,\n DictionaryData,\n DictionaryDocument,\n} from '@/types/dictionary.types';\nimport type { Project } from '@/types/project.types';\n\n/**\n * Finds dictionaries 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 * @param includeContent - Whether to include the dictionary content.\n * @returns List of dictionaries matching the filters.\n */\nexport const findDictionaries = async (\n filters: DictionaryFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>,\n includeContent = true\n): Promise<DictionaryDocument[]> => {\n try {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the filters\n { $match: filters },\n\n // Stage 2: Sort if provided (default handled in filter builder)\n ...(sortOptions && Object.keys(sortOptions).length > 0\n ? [{ $sort: sortOptions }]\n : []),\n\n // Stage 3: Skip for pagination\n { $skip: skip },\n\n // Stage 4: Limit the number of documents\n { $limit: limit },\n\n // Stage 5: Project to include/exclude content\n ...(!includeContent ? [{ $project: { content: 0 } }] : []),\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n } catch (error) {\n logger.error('Error fetching dictionaries:', error);\n throw error;\n }\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\n/**\n * Finds a dictionary by its ID and includes the 'versions' field.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID with available versions.\n */\nexport const getDictionaryById = async (\n dictionaryId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const id = Types.ObjectId.isValid(dictionaryId as string)\n ? new Types.ObjectId(dictionaryId as string)\n : dictionaryId;\n\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by ID\n { $match: { _id: id } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries.length) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return new DictionaryModel(dictionaries[0]);\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryKey - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\nexport const getDictionaryByKey = async (\n dictionaryKey: string,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const dictionaries = await getDictionariesByKeys([dictionaryKey], projectId);\n\n return dictionaries[0];\n};\n\nexport const getDictionariesByKeys = async (\n dictionaryKeys: string[],\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by key\n { $match: { key: { $in: dictionaryKeys }, projectIds: projectId } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries) {\n throw new GenericError('DICTIONARY_NOT_FOUND', {\n dictionaryKeys,\n projectId,\n });\n }\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\nexport const getDictionariesByTags = async (\n tags: string[],\n projectId: string | Project['id']\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by tags\n {\n $match: {\n tags: { $in: tags },\n projectIds: projectId,\n },\n },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\n/**\n * Counts the total number of dictionaries that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of dictionaries.\n */\nexport const countDictionaries = async (\n filters: DictionaryFilters\n): Promise<number> => {\n const result = await DictionaryModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('DICTIONARY_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new dictionary in the database.\n * @param dictionary - The dictionary data to create.\n * @returns The created dictionary.\n */\nexport const createDictionary = async (\n dictionary: DictionaryData\n): Promise<DictionaryDocument> => {\n const errors = await validateDictionary(dictionary);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n errors,\n });\n }\n\n return await DictionaryModel.create(dictionary);\n};\n\n/**\n * Updates an existing dictionary in the database by its ID.\n * @param dictionaryId - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryById = async (\n dictionaryId: string | Types.ObjectId,\n dictionary: Partial<Dictionary>\n): Promise<DictionaryDocument> => {\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as unknown as Partial<Dictionary>;\n\n const updatedKeys = Object.keys(dictionaryToUpdate) as DictionaryFields;\n const errors = await validateDictionary(dictionaryToUpdate, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n dictionaryId,\n errors,\n });\n }\n\n const result = await DictionaryModel.updateOne(\n { _id: dictionaryId },\n dictionaryToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryId });\n }\n\n const updatedDictionary = await getDictionaryById(dictionaryId);\n\n return updatedDictionary;\n};\n\n/**\n * Updates an existing dictionary in the database by its key.\n * @param dictionaryKey - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryByKey = async (\n dictionaryKey: string,\n dictionary: Partial<Dictionary>,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const existing = await DictionaryModel.findOne({\n key: String(dictionaryKey),\n projectIds: projectId,\n });\n\n if (!existing) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryKey });\n }\n\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as Partial<Dictionary>;\n\n // Optional: run your validateDictionary on dictionaryToUpdate here\n\n // Apply updated fields onto the existing doc\n Object.assign(existing, dictionaryToUpdate);\n\n // Mongoose cannot track deep Map mutations done via Object.assign, so we\n // must explicitly mark 'content' as modified, otherwise the new versioned\n // content is silently dropped and the document is saved unchanged.\n existing.markModified('content');\n\n // Save – this will trigger timestamps on parent + subdocs\n await existing.save();\n\n return existing;\n};\n\n/**\n * Deletes a dictionary from the database by its ID.\n * @param dictionaryId - The ID of the dictionary to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteDictionaryById = async (\n dictionaryId: string\n): Promise<DictionaryDocument> => {\n const dictionary = await DictionaryModel.findByIdAndDelete(dictionaryId);\n\n if (!dictionary) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return dictionary;\n};\n\n// Function to extract the numeric part of the version\nconst getVersionNumber = (version: string): number => {\n const match = version.match(/^v(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid version format: ${version}`);\n }\n return parseInt(match[1], 10);\n};\n\nexport const incrementVersion = (dictionary: Dictionary): string => {\n const VERSION_PREFIX = 'v';\n\n const versions = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versions[versions.length - 1];\n\n // Start with the next version number\n let newNumber = getVersionNumber(lastVersion) + 1;\n let newVersion = `${VERSION_PREFIX}${newNumber}`;\n\n // Loop until a unique version is found\n while (versions.includes(newVersion)) {\n newNumber += 1;\n newVersion = `${VERSION_PREFIX}${newNumber}`;\n }\n\n return newVersion;\n};\n\n/**\n * Creates demo dictionaries for a project.\n * @param projectIds - List of project IDs.\n * @param creatorId - The ID of the user creating the demo content.\n */\nexport const createDemoDictionaries = async (\n projectIds: string[],\n creatorId: Types.ObjectId | string\n): Promise<void> => {\n const demoDictionaries = getDemoDictionaries(projectIds, creatorId);\n\n for (const dictionary of demoDictionaries) {\n await createDictionary(dictionary);\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"dictionary.service.mjs","names":[],"sources":["../../../src/services/dictionary.service.ts"],"sourcesContent":["import { logger } from '@logger';\nimport { DictionaryModel } from '@schemas/dictionary.schema';\nimport { getDemoDictionaries } from '@utils/demoDictionaries';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport type { DictionaryFilters } from '@utils/filtersAndPagination/getDictionaryFiltersAndPagination';\nimport { removeObjectKeys } from '@utils/removeObjectKeys';\nimport {\n type DictionaryFields,\n validateDictionary,\n} from '@utils/validation/validateDictionary';\nimport { Types } from 'mongoose';\nimport type {\n Dictionary,\n DictionaryData,\n DictionaryDocument,\n} from '@/types/dictionary.types';\nimport type { Project } from '@/types/project.types';\n\n/**\n * Finds dictionaries 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 * @param includeContent - Whether to include the dictionary content.\n * @returns List of dictionaries matching the filters.\n */\nexport const findDictionaries = async (\n filters: DictionaryFilters,\n skip = 0,\n limit = 100,\n sortOptions?: Record<string, 1 | -1>,\n includeContent = true\n): Promise<DictionaryDocument[]> => {\n try {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the filters\n { $match: filters },\n\n // Stage 2: Sort if provided (default handled in filter builder)\n ...(sortOptions && Object.keys(sortOptions).length > 0\n ? [{ $sort: sortOptions }]\n : []),\n\n // Stage 3: Skip for pagination\n { $skip: skip },\n\n // Stage 4: Limit the number of documents\n { $limit: limit },\n\n // Stage 5: Project to include/exclude content\n ...(!includeContent ? [{ $project: { content: 0 } }] : []),\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n } catch (error) {\n logger.error('Error fetching dictionaries:', error);\n throw error;\n }\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\n/**\n * Finds a dictionary by its ID and includes the 'versions' field.\n * @param dictionaryId - The ID of the dictionary to find.\n * @returns The dictionary matching the ID with available versions.\n */\nexport const getDictionaryById = async (\n dictionaryId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const id = Types.ObjectId.isValid(dictionaryId as string)\n ? new Types.ObjectId(dictionaryId as string)\n : dictionaryId;\n\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by ID\n { $match: { _id: id } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries.length) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return new DictionaryModel(dictionaries[0]);\n};\n\n/**\n * Finds a dictionary by its ID.\n * @param dictionaryKey - The ID of the dictionary to find.\n * @returns The dictionary matching the ID.\n */\nexport const getDictionaryByKey = async (\n dictionaryKey: string,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const dictionaries = await getDictionariesByKeys([dictionaryKey], projectId);\n\n return dictionaries[0];\n};\n\nexport const getDictionariesByKeys = async (\n dictionaryKeys: string[],\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by key\n { $match: { key: { $in: dictionaryKeys }, projectIds: projectId } },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n if (!dictionaries) {\n throw new GenericError('DICTIONARY_NOT_FOUND', {\n dictionaryKeys,\n projectId,\n });\n }\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\nexport const getDictionariesByTags = async (\n tags: string[],\n projectId: string | Project['id']\n): Promise<DictionaryDocument[]> => {\n const dictionaries = await DictionaryModel.aggregate<DictionaryDocument>([\n // Stage 1: Match the document by tags\n {\n $match: {\n tags: { $in: tags },\n projectIds: projectId,\n },\n },\n\n // Stage 2: Add the 'versions' field\n {\n $addFields: {\n versions: {\n $map: {\n input: { $objectToArray: '$content' },\n as: 'version',\n in: '$$version.k',\n },\n },\n },\n },\n ]);\n\n const formattedResults = dictionaries.map(\n (result) => new DictionaryModel(result)\n );\n\n return formattedResults;\n};\n\n/**\n * Counts the total number of dictionaries that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of dictionaries.\n */\nexport const countDictionaries = async (\n filters: DictionaryFilters\n): Promise<number> => {\n const result = await DictionaryModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('DICTIONARY_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new dictionary in the database.\n * @param dictionary - The dictionary data to create.\n * @returns The created dictionary.\n */\nexport const createDictionary = async (\n dictionary: DictionaryData\n): Promise<DictionaryDocument> => {\n const errors = await validateDictionary(dictionary);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n errors,\n });\n }\n\n return await DictionaryModel.create(dictionary);\n};\n\n/**\n * Updates an existing dictionary in the database by its ID.\n * @param dictionaryId - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryById = async (\n dictionaryId: string | Types.ObjectId,\n dictionary: Partial<Dictionary>\n): Promise<DictionaryDocument> => {\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as unknown as Partial<Dictionary>;\n\n const updatedKeys = Object.keys(dictionaryToUpdate) as DictionaryFields;\n const errors = await validateDictionary(dictionaryToUpdate, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('DICTIONARY_INVALID_FIELDS', {\n dictionaryId,\n errors,\n });\n }\n\n const result = await DictionaryModel.updateOne(\n { _id: dictionaryId },\n dictionaryToUpdate\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryId });\n }\n\n const updatedDictionary = await getDictionaryById(dictionaryId);\n\n return updatedDictionary;\n};\n\n/**\n * Updates an existing dictionary in the database by its key.\n * @param dictionaryKey - The ID of the dictionary to update.\n * @param dictionary - The updated dictionary data.\n * @returns The updated dictionary.\n */\nexport const updateDictionaryByKey = async (\n dictionaryKey: string,\n dictionary: Partial<Dictionary>,\n projectId: string | Types.ObjectId\n): Promise<DictionaryDocument> => {\n const existing = await DictionaryModel.findOne({\n key: String(dictionaryKey),\n projectIds: projectId,\n });\n\n if (!existing) {\n throw new GenericError('DICTIONARY_UPDATE_FAILED', { dictionaryKey });\n }\n\n const dictionaryObject = ensureMongoDocumentToObject(dictionary);\n const dictionaryToUpdate = removeObjectKeys(dictionaryObject, [\n 'id',\n ]) as Partial<Dictionary>;\n\n // Optional: run your validateDictionary on dictionaryToUpdate here\n\n // Apply updated fields onto the existing doc\n Object.assign(existing, dictionaryToUpdate);\n\n // Mongoose cannot track deep Map mutations done via Object.assign, so we\n // must explicitly mark 'content' as modified, otherwise the new versioned\n // content is silently dropped and the document is saved unchanged.\n existing.markModified('content');\n\n // Save – this will trigger timestamps on parent + subdocs\n await existing.save();\n\n return existing;\n};\n\n/**\n * Deletes a dictionary from the database by its ID.\n * @param dictionaryId - The ID of the dictionary to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteDictionaryById = async (\n dictionaryId: string\n): Promise<DictionaryDocument> => {\n const dictionary = await DictionaryModel.findByIdAndDelete(dictionaryId);\n\n if (!dictionary) {\n throw new GenericError('DICTIONARY_NOT_FOUND', { dictionaryId });\n }\n\n return dictionary;\n};\n\n// Function to extract the numeric part of the version\nconst getVersionNumber = (version: string): number => {\n const match = version.match(/^v(\\d+)$/);\n if (!match) {\n throw new Error(`Invalid version format: ${version}`);\n }\n return parseInt(match[1], 10);\n};\n\nexport const incrementVersion = (dictionary: Dictionary): string => {\n const VERSION_PREFIX = 'v';\n\n const versions = [...(dictionary.content.keys() ?? [])];\n const lastVersion = versions[versions.length - 1];\n\n // Start with the next version number\n let newNumber = getVersionNumber(lastVersion) + 1;\n let newVersion = `${VERSION_PREFIX}${newNumber}`;\n\n // Loop until a unique version is found\n while (versions.includes(newVersion)) {\n newNumber += 1;\n newVersion = `${VERSION_PREFIX}${newNumber}`;\n }\n\n return newVersion;\n};\n\n/**\n * Creates demo dictionaries for a project.\n * @param projectIds - List of project IDs.\n * @param creatorId - The ID of the user creating the demo content.\n */\nexport const createDemoDictionaries = async (\n projectIds: string[],\n creatorId: Types.ObjectId | string\n): Promise<void> => {\n const demoDictionaries = getDemoDictionaries(projectIds, creatorId);\n\n for (const dictionary of demoDictionaries) {\n await createDictionary(dictionary);\n }\n};\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../logger/index.mjs";
2
- import { AccountModel } from "../models/account.model.mjs";
2
+ import { AccountModel } from "../schemas/account.schema.mjs";
3
3
  import { configurationFilesCandidates } from "@intlayer/config/node";
4
4
  import { Octokit } from "@octokit/rest";
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"github.service.mjs","names":[],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport { AccountModel } from '@models/account.model';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const account = await AccountModel.findOne({\n userId,\n providerId: 'github',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2026-03-10',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"github.service.mjs","names":[],"sources":["../../../src/services/github.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport type { RestEndpointMethodTypes } from '@octokit/rest';\nimport { Octokit } from '@octokit/rest';\nimport { AccountModel } from '@schemas/account.schema';\nimport type { Project } from '@/types/project.types';\n\nexport type GitHubRepository =\n RestEndpointMethodTypes['repos']['listForAuthenticatedUser']['response']['data'][0];\nexport type GitHubFileContent =\n RestEndpointMethodTypes['repos']['getContent']['response']['data'];\n\nexport const getAuthorizationUrl = (\n redirectUri: string,\n login?: string\n): string => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n\n if (!clientId) {\n throw new Error('GitHub Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n scope: 'repo',\n state: 'github_oauth',\n redirect_uri: redirectUri,\n });\n\n if (login) {\n params.append('login', login);\n }\n\n return `https://github.com/login/oauth/authorize?${params.toString()}`;\n};\n\nexport const exchangeCodeForToken = async (code: string): Promise<string> => {\n const clientId = process.env.GITHUB_CLIENT_ID;\n const clientSecret = process.env.GITHUB_CLIENT_SECRET;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitHub OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(\n 'https://github.com/login/oauth/access_token',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n }),\n }\n );\n\n if (!response.ok) {\n throw new Error(`GitHub token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitHub token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitHub code for token:', error);\n throw error;\n }\n};\n\nexport const getUserRepos = async (\n accessToken: string\n): Promise<GitHubRepository[]> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n const { data } = await octokit.rest.repos.listForAuthenticatedUser({\n sort: 'updated',\n per_page: 100,\n visibility: 'all',\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n return data;\n } catch (error) {\n logger.error('Error fetching GitHub repositories:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a repository (Recursively).\n * Returns an array of file paths found (e.g. ['intlayer.config.ts', 'apps/web/intlayer.config.js']).\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n owner: string,\n repo: string,\n branch: string = 'main'\n): Promise<string[]> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n });\n\n // Use Git Tree API to get all files recursively\n // This allows finding configs in monorepos/subfolders\n const { data } = await octokit.rest.git.getTree({\n owner,\n repo,\n tree_sha: branch,\n recursive: 'true',\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (!data.tree || !Array.isArray(data.tree)) {\n return [];\n }\n\n // Filter files that match the configuration candidates\n // We check if the path ends with one of the candidate filenames\n const foundFiles = data.tree\n .filter((item) => {\n if (item.type !== 'blob' || !item.path) return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path?.endsWith(candidate)\n );\n })\n .map((item) => item.path as string); // Return the full path (e.g., 'packages/app/intlayer.config.ts')\n\n return foundFiles;\n } catch (error: any) {\n // If branch doesn't exist or repo is empty\n if (error.status === 404 || error.status === 409) return [];\n\n logger.error('Error checking intlayer configuration:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n owner: string,\n repo: string,\n path: string,\n branch: string = 'main'\n): Promise<string | null> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Octokit types are union types (file | dir | submodule), we need to check if it's a file\n if (Array.isArray(data) || !('content' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n // GitHub returns content in base64, we must decode it to read the actual code\n const decodedContent = Buffer.from(data.content, 'base64').toString(\n 'utf-8'\n );\n\n return decodedContent;\n } catch (error: any) {\n if (error.status === 404) return null;\n\n logger.error('Error fetching repository file contents:', error);\n throw error;\n }\n};\n\nexport const getGitHubTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const account = await AccountModel.findOne({\n userId,\n providerId: 'github',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitHub token from DB:', error);\n return null;\n }\n};\n\ntype DispatchEventOptions = {\n project: Project;\n eventType?: string;\n payload?: Record<string, any>;\n};\n\nexport const triggerGithubDispatch = async ({\n project,\n eventType = 'intlayer_cms_update',\n payload = {},\n}: DispatchEventOptions) => {\n const { repository, oAuth2Access } = project;\n\n if (!repository || repository.provider !== 'github') {\n throw new Error('Project is not connected to a GitHub repository.');\n }\n\n // Get the valid Access Token\n // Assuming the first token is the active one, or implement logic to find the specific user's token\n const tokenData = oAuth2Access?.[0];\n const accessToken = tokenData?.accessToken?.[0]; // Assuming array of tokens\n\n if (!accessToken) {\n throw new Error('No valid OAuth2 access token found for GitHub.');\n }\n\n const { owner, repository: repoName } = repository;\n const url = `https://api.github.com/repos/${owner}/${repoName}/dispatches`;\n\n try {\n // 2. Send the Dispatch Event\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/vnd.github+json',\n 'X-GitHub-Api-Version': '2026-03-10',\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n event_type: eventType,\n client_payload: {\n ...payload,\n projectId: project.id,\n timestamp: Date.now(),\n },\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`GitHub API Error: ${response.status} - ${errorText}`);\n }\n\n logger.info(\n `Successfully triggered GitHub Action '${eventType}' for ${owner}/${repoName}`\n );\n return true;\n } catch (error) {\n logger.error(error);\n throw error;\n }\n};\n\n/**\n * Check if a GitHub workflow file exists\n */\nexport const checkWorkflowFileExists = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n branch: string = 'main'\n): Promise<boolean> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking workflow file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitHub workflow file\n */\nexport const createWorkflowFile = async (\n accessToken: string,\n owner: string,\n repo: string,\n filename: string,\n content: string,\n branch: string = 'main',\n message: string = 'Add Intlayer CI workflow'\n): Promise<void> => {\n try {\n const octokit = new Octokit({\n auth: accessToken,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n // Check if file exists to get SHA for update\n let sha: string | undefined;\n try {\n const { data } = await octokit.rest.repos.getContent({\n owner,\n repo,\n path: filename,\n ref: branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n });\n\n if (Array.isArray(data) || !('sha' in data)) {\n throw new Error('Path points to a directory, not a file');\n }\n\n sha = data.sha;\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n // Encode content to base64\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n await octokit.rest.repos.createOrUpdateFileContents({\n owner,\n repo,\n path: filename,\n message,\n content: encodedContent,\n branch,\n headers: {\n 'X-GitHub-Api-Version': '2026-03-10',\n },\n ...(sha && { sha }), // Include SHA if updating existing file\n });\n\n logger.info(\n `Successfully ${sha ? 'updated' : 'created'} workflow file ${filename} for ${owner}/${repo}`\n );\n } catch (error) {\n logger.error('Error creating/updating workflow file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { logger } from "../logger/index.mjs";
2
- import { AccountModel } from "../models/account.model.mjs";
2
+ import { AccountModel } from "../schemas/account.schema.mjs";
3
3
  import { configurationFilesCandidates } from "@intlayer/config/node";
4
4
 
5
5
  //#region src/services/gitlab.service.ts
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab.service.mjs","names":[],"sources":["../../../src/services/gitlab.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport { AccountModel } from '@models/account.model';\n\nconst GITLAB_DEFAULT_URL = 'https://gitlab.com';\n\nexport type GitLabProject = {\n id: number;\n name: string;\n path_with_namespace: string;\n web_url: string;\n default_branch: string;\n visibility: string;\n last_activity_at: string;\n namespace: {\n id: number;\n name: string;\n path: string;\n };\n};\n\nexport type GitLabTreeItem = {\n id: string;\n name: string;\n type: 'tree' | 'blob';\n path: string;\n mode: string;\n};\n\n/**\n * Get GitLab authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (\n redirectUri: string,\n instanceUrl?: string,\n login?: string\n): string => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId) {\n throw new Error('GitLab Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: 'api read_repository',\n state: 'gitlab_oauth',\n });\n\n if (login) {\n params.append('login_hint', login);\n }\n\n return `${baseUrl}/oauth/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange GitLab authorization code for access token\n */\nexport const exchangeCodeForToken = async (\n code: string,\n redirectUri: string,\n instanceUrl?: string\n): Promise<string> => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const clientSecret = process.env.GITLAB_CLIENT_SECRET;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitLab OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(`${baseUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`GitLab token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitLab token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitLab code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's GitLab projects/repositories\n */\nexport const getUserProjects = async (\n accessToken: string,\n instanceUrl?: string\n): Promise<GitLabProject[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const response = await fetch(\n `${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch GitLab projects: ${response.statusText}`\n );\n }\n\n const projects: GitLabProject[] = await response.json();\n return projects;\n } catch (error) {\n logger.error('Error fetching GitLab projects:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a GitLab repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n projectId: number,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n // Use GitLab's repository tree API with recursive option\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const tree: GitLabTreeItem[] = await response.json();\n\n // Filter files that match the configuration candidates\n const foundFiles = tree\n .filter((item) => {\n if (item.type !== 'blob') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on GitLab:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from GitLab and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n projectId: number,\n path: string,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string | null> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(path);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching GitLab file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get GitLab access token from user's linked account\n */\nexport const getGitLabTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const account = await AccountModel.findOne({\n userId,\n providerId: 'gitlab',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitLab token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a GitLab CI pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n branch: string = 'main',\n instanceUrl?: string\n): Promise<boolean> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitLab CI pipeline file\n */\nexport const createPipelineFile = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n content: string,\n branch: string = 'main',\n instanceUrl?: string,\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n // Check if file exists to get content_sha256 for update\n let existingContentSha: string | undefined;\n try {\n const checkResponse = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (checkResponse.ok) {\n const fileData = await checkResponse.json();\n existingContentSha = fileData.content_sha256;\n }\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n const body: any = {\n branch,\n content: encodedContent,\n commit_message: message,\n encoding: 'base64',\n };\n\n if (existingContentSha) {\n body.last_commit_id = existingContentSha;\n }\n\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to create/update pipeline file: ${errorText}`);\n }\n\n logger.info(\n `Successfully ${existingContentSha ? 'updated' : 'created'} pipeline file ${filename} for project ${projectId}`\n );\n } catch (error) {\n logger.error('Error creating/updating pipeline file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"gitlab.service.mjs","names":[],"sources":["../../../src/services/gitlab.service.ts"],"sourcesContent":["import { configurationFilesCandidates } from '@intlayer/config/node';\nimport { logger } from '@logger';\nimport { AccountModel } from '@schemas/account.schema';\n\nconst GITLAB_DEFAULT_URL = 'https://gitlab.com';\n\nexport type GitLabProject = {\n id: number;\n name: string;\n path_with_namespace: string;\n web_url: string;\n default_branch: string;\n visibility: string;\n last_activity_at: string;\n namespace: {\n id: number;\n name: string;\n path: string;\n };\n};\n\nexport type GitLabTreeItem = {\n id: string;\n name: string;\n type: 'tree' | 'blob';\n path: string;\n mode: string;\n};\n\n/**\n * Get GitLab authorization URL for OAuth flow\n */\nexport const getAuthorizationUrl = (\n redirectUri: string,\n instanceUrl?: string,\n login?: string\n): string => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId) {\n throw new Error('GitLab Client ID is not configured');\n }\n\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: 'code',\n scope: 'api read_repository',\n state: 'gitlab_oauth',\n });\n\n if (login) {\n params.append('login_hint', login);\n }\n\n return `${baseUrl}/oauth/authorize?${params.toString()}`;\n};\n\n/**\n * Exchange GitLab authorization code for access token\n */\nexport const exchangeCodeForToken = async (\n code: string,\n redirectUri: string,\n instanceUrl?: string\n): Promise<string> => {\n const clientId = process.env.GITLAB_CLIENT_ID;\n const clientSecret = process.env.GITLAB_CLIENT_SECRET;\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n if (!clientId || !clientSecret) {\n throw new Error('GitLab OAuth credentials are not configured');\n }\n\n try {\n const response = await fetch(`${baseUrl}/oauth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify({\n client_id: clientId,\n client_secret: clientSecret,\n code,\n grant_type: 'authorization_code',\n redirect_uri: redirectUri,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`GitLab token exchange failed: ${response.statusText}`);\n }\n\n const data = await response.json();\n\n if (data.error) {\n throw new Error(`GitLab token error: ${data.error_description}`);\n }\n\n return data.access_token;\n } catch (error) {\n logger.error('Error exchanging GitLab code for token:', error);\n throw error;\n }\n};\n\n/**\n * Get user's GitLab projects/repositories\n */\nexport const getUserProjects = async (\n accessToken: string,\n instanceUrl?: string\n): Promise<GitLabProject[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const response = await fetch(\n `${baseUrl}/api/v4/projects?membership=true&order_by=last_activity_at&per_page=100`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch GitLab projects: ${response.statusText}`\n );\n }\n\n const projects: GitLabProject[] = await response.json();\n return projects;\n } catch (error) {\n logger.error('Error fetching GitLab projects:', error);\n throw error;\n }\n};\n\n/**\n * Check if valid intlayer configuration files exist in a GitLab repository (Recursively).\n * Returns an array of file paths found.\n */\nexport const checkIntlayerConfig = async (\n accessToken: string,\n projectId: number,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string[]> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n // Use GitLab's repository tree API with recursive option\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/tree?ref=${encodeURIComponent(branch)}&recursive=true&per_page=10000`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return [];\n throw new Error(\n `Failed to fetch repository tree: ${response.statusText}`\n );\n }\n\n const tree: GitLabTreeItem[] = await response.json();\n\n // Filter files that match the configuration candidates\n const foundFiles = tree\n .filter((item) => {\n if (item.type !== 'blob') return false;\n return (configurationFilesCandidates as readonly string[]).some(\n (candidate) => item.path.endsWith(candidate)\n );\n })\n .map((item) => item.path);\n\n return foundFiles;\n } catch (error: any) {\n if (error.status === 404) return [];\n logger.error('Error checking intlayer configuration on GitLab:', error);\n return [];\n }\n};\n\n/**\n * Get repository file contents from GitLab and decode it\n */\nexport const getRepositoryFileContents = async (\n accessToken: string,\n projectId: number,\n path: string,\n branch: string = 'main',\n instanceUrl?: string\n): Promise<string | null> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(path);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (!response.ok) {\n if (response.status === 404) return null;\n throw new Error(`Failed to fetch file contents: ${response.statusText}`);\n }\n\n const content = await response.text();\n return content;\n } catch (error: any) {\n if (error.status === 404) return null;\n logger.error('Error fetching GitLab file contents:', error);\n throw error;\n }\n};\n\n/**\n * Get GitLab access token from user's linked account\n */\nexport const getGitLabTokenFromUser = async (\n userId: string\n): Promise<string | null> => {\n try {\n const account = await AccountModel.findOne({\n userId,\n providerId: 'gitlab',\n });\n\n if (!account) {\n return null;\n }\n\n const accessToken = account.accessToken || account.access_token;\n\n return accessToken || null;\n } catch (error) {\n logger.error('Error retrieving GitLab token from DB:', error);\n return null;\n }\n};\n\n/**\n * Check if a GitLab CI pipeline file exists\n */\nexport const checkPipelineFileExists = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n branch: string = 'main',\n instanceUrl?: string\n): Promise<boolean> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (response.status === 404) return false;\n if (!response.ok) {\n throw new Error(`Failed to check file existence: ${response.statusText}`);\n }\n\n return true;\n } catch (error: any) {\n if (error.status === 404) return false;\n logger.error('Error checking pipeline file existence:', error);\n throw error;\n }\n};\n\n/**\n * Create or update a GitLab CI pipeline file\n */\nexport const createPipelineFile = async (\n accessToken: string,\n projectId: number,\n filename: string = '.gitlab-ci.yml',\n content: string,\n branch: string = 'main',\n instanceUrl?: string,\n message: string = 'Add Intlayer CI pipeline'\n): Promise<void> => {\n const baseUrl = instanceUrl || GITLAB_DEFAULT_URL;\n\n try {\n const encodedPath = encodeURIComponent(filename);\n const encodedContent = Buffer.from(content, 'utf-8').toString('base64');\n\n // Check if file exists to get content_sha256 for update\n let existingContentSha: string | undefined;\n try {\n const checkResponse = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}?ref=${encodeURIComponent(branch)}`,\n {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n Accept: 'application/json',\n },\n }\n );\n\n if (checkResponse.ok) {\n const fileData = await checkResponse.json();\n existingContentSha = fileData.content_sha256;\n }\n } catch (error: any) {\n if (error.status !== 404) {\n throw error;\n }\n // File doesn't exist, will create new one\n }\n\n const body: any = {\n branch,\n content: encodedContent,\n commit_message: message,\n encoding: 'base64',\n };\n\n if (existingContentSha) {\n body.last_commit_id = existingContentSha;\n }\n\n const response = await fetch(\n `${baseUrl}/api/v4/projects/${projectId}/repository/files/${encodedPath}`,\n {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n },\n body: JSON.stringify(body),\n }\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to create/update pipeline file: ${errorText}`);\n }\n\n logger.info(\n `Successfully ${existingContentSha ? 'updated' : 'created'} pipeline file ${filename} for project ${projectId}`\n );\n } catch (error) {\n logger.error('Error creating/updating pipeline file:', error);\n throw error;\n }\n};\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { ensureMongoDocumentToObject } from "../utils/ensureMongoDocumentToObject.mjs";
2
2
  import { GenericError } from "../utils/errors/ErrorsClass.mjs";
3
3
  import { getOrganizationById } from "./organization.service.mjs";
4
- import { ProjectModel } from "../models/project.model.mjs";
4
+ import { ProjectModel } from "../schemas/project.schema.mjs";
5
5
  import { getUserById } from "./user.service.mjs";
6
6
  import { mapUserToAPI } from "../utils/mapper/user.mjs";
7
7
  import { mapOrganizationToAPI } from "../utils/mapper/organization.mjs";
8
8
  import { mapProjectToAPI } from "../utils/mapper/project.mjs";
9
- import { OAuth2AccessTokenModel } from "../models/oAuth2.model.mjs";
9
+ import { OAuth2AccessTokenModel } from "../schemas/oAuth2.schema.mjs";
10
10
  import { getTokenExpireAt, shouldExtendOAuth2Token } from "../utils/oAuth2.mjs";
11
11
  import { randomBytes } from "node:crypto";
12
12
 
@@ -1 +1 @@
1
- {"version":3,"file":"oAuth2.service.mjs","names":[],"sources":["../../../src/services/oAuth2.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { OAuth2AccessTokenModel } from '@models/oAuth2.model';\nimport { ProjectModel } from '@models/project.model';\nimport type { Callback, Client } from '@node-oauth/oauth2-server';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport { getTokenExpireAt, shouldExtendOAuth2Token } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\nimport type { Organization } from '@/types/organization.types';\nimport type {\n OAuth2Access,\n OAuth2AccessContext,\n Project,\n ProjectDocument,\n} from '@/types/project.types';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\nimport type { Token } from '../schemas/oAuth2.schema';\nimport { getOrganizationById } from './organization.service';\nimport { getUserById } from './user.service';\n\n/**\n * Function to generate client credentials\n *\n * @returns The client id and client secret\n */\nexport const generateClientCredentials = (): {\n clientId: string;\n clientSecret: string;\n} => {\n const clientId = randomBytes(16).toString('hex'); // Generate a 16 character hexadecimal string\n const clientSecret = randomBytes(32).toString('hex'); // Generate a 32 character hexadecimal string\n\n return { clientId, clientSecret };\n};\n\n/**\n * Method to get the client and the project\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The an object containing the client, the rights and the project or false if not found\n */\nexport const getClientAndProjectByClientId = async (\n clientId: string\n): Promise<\n | {\n client: Client;\n oAuth2Access: OAuth2Access;\n project: ProjectDocument;\n grants: Token['grants'];\n }\n | false\n> => {\n const project = await ProjectModel.findOne({\n 'oAuth2Access.clientId': String(clientId),\n });\n\n if (!project) {\n return false;\n }\n\n const oAuth2Access = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!oAuth2Access) {\n return false;\n }\n\n const formattedClient: Client = {\n id: oAuth2Access.clientId,\n clientId,\n clientSecret: oAuth2Access.clientSecret,\n grants: ['client_credentials'],\n };\n\n return {\n client: formattedClient,\n oAuth2Access,\n grants: oAuth2Access.grants,\n project,\n };\n};\n\n/**\n * Get the client and verify that the client secret is correct\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The client or false if not found\n */\nexport const getClient = async (\n clientId: string,\n clientSecret: string\n): Promise<Client | false> => {\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client } = result;\n\n if (!client || client.clientSecret !== clientSecret) {\n return false;\n }\n\n return client;\n};\n\n/**\n * Format an OAuth2Token\n *\n * @param token - The token to format\n * @param client - The client\n * @param user - The user\n * @param project - The project\n * @param organization - The organization\n * @param grants - The grants\n * @returns The formatted token\n */\nexport const formatOAuth2Token = (\n token: Token,\n client: Client,\n user: UserAPI,\n project: Project,\n organization: Organization,\n grants: Token['grants']\n): OAuth2Token => {\n const { clientId, userId, ...restToken } = token;\n\n if (String(userId) !== String(user.id)) {\n throw new GenericError('USER_ID_MISMATCH');\n }\n\n const formattedToken: OAuth2Token = {\n ...restToken,\n client,\n user: mapUserToAPI(user),\n organization: mapOrganizationToAPI(organization),\n project: mapProjectToAPI(project),\n accessToken: token.accessToken,\n accessTokenExpiresAt: token.accessTokenExpiresAt ?? new Date('999-99-99'),\n grants,\n };\n\n return formattedToken;\n};\n\n/**\n * Format a auth token for the database\n *\n * @param token - The oAuth2 token to format\n * @param clientId - The client ID\n * @param userId - The user ID\n * @returns\n */\nexport const formatDBToken = (\n token: OAuth2Token,\n clientId: Client['id'],\n userId: User['id'] | string\n): Token => {\n const formattedToken: Token = {\n id: token.id,\n clientId: clientId,\n userId: userId as Types.ObjectId,\n accessToken: token.accessToken,\n expiresIn: token.accessTokenExpiresAt ?? getTokenExpireAt(),\n };\n\n return formattedToken;\n};\n\n/**\n * Method to save the token\n *\n * @param token - The token\n * @param client - The client\n * @param user - The user\n * @returns The saved token or false if not saved\n */\nexport const saveToken = async (\n token: OAuth2Token,\n client: Client,\n user: UserAPI\n): Promise<OAuth2Token | false> => {\n const formattedAccessToken: Token = formatDBToken(token, client.id, user.id);\n\n const result = await OAuth2AccessTokenModel.create(formattedAccessToken);\n\n if (!result) {\n return false;\n }\n\n const result2 = await getClientAndProjectByClientId(result.clientId);\n\n if (!result2) {\n return false;\n }\n\n const { project } = result2;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedResult = formatOAuth2Token(\n formattedAccessToken,\n client,\n user,\n project,\n organization,\n token.rights\n );\n return formattedResult;\n};\n\n/**\n * Sliding-refresh: push the token's expiry forward when it has been used\n * within the refresh threshold. Idempotent and cheap when no extension is due.\n */\nexport const extendOAuth2AccessToken = async (\n accessToken: string\n): Promise<Date> => {\n const nextExpiresAt = getTokenExpireAt();\n await OAuth2AccessTokenModel.updateOne(\n { accessToken: String(accessToken) },\n { $set: { accessTokenExpiresAt: nextExpiresAt, expiresIn: nextExpiresAt } }\n );\n return nextExpiresAt;\n};\n\n/**\n * Method to get the access token\n *\n * @param accessToken - The access token\n * @returns The access token or false if not found\n */\nexport const getAccessToken = async (\n accessToken: string\n): Promise<OAuth2Token | false> => {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken: String(accessToken),\n });\n\n if (!token) {\n return false;\n }\n\n // Slide the expiry forward when this active token is approaching its\n // deadline so a long-lived integration doesn't have to re-authenticate.\n const currentExpiresAt = token.accessTokenExpiresAt ?? token.expiresIn;\n if (currentExpiresAt && shouldExtendOAuth2Token(currentExpiresAt)) {\n const nextExpiresAt = await extendOAuth2AccessToken(accessToken);\n token.accessTokenExpiresAt = nextExpiresAt;\n token.expiresIn = nextExpiresAt;\n }\n\n const { userId, clientId } = token;\n\n const user = await getUserById(userId);\n\n if (!user) {\n return false;\n }\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client, project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedAccessToken = formatOAuth2Token(\n token,\n client,\n user,\n project,\n organization,\n grants\n );\n\n return formattedAccessToken;\n};\n\n/**\n * Method to get the user from the client\n *\n * @param client - The client\n * @returns The user or false if not found\n */\nexport const getUserFromClient = async (\n client: Client\n): Promise<UserDocument | false> => {\n const response = await getClientAndProjectByClientId(client.id);\n\n if (!response) {\n return false;\n }\n\n const { userId } = response.oAuth2Access;\n\n if (!userId) {\n return false;\n }\n\n const user = await getUserById(userId);\n\n return user ?? false;\n};\n\n/**\n * Method to verify the permissions (grants)\n *\n * @param token - The token\n * @param scope - The scope\n * @returns True if the token has the required scope, false otherwise\n */\nexport const verifyScope = async (\n _token: OAuth2Token,\n _scope: string,\n _callback?: Callback<boolean> | undefined\n): Promise<boolean> => {\n // Implement the verification of scopes if necessary\n return true;\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const validateOAuth2AccessToken = async (\n accessToken: string\n): Promise<Token> => {\n try {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken,\n });\n\n if (!token) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n // Check if token is expired\n if (new Date() > new Date(token.expiresIn)) {\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n return ensureMongoDocumentToObject(token);\n } catch (_error) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const getOAuth2AccessTokenContext = async (\n token: Token\n): Promise<OAuth2AccessContext> => {\n const { userId, clientId } = token;\n\n const user = await getUserById(String(userId));\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const { project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n return {\n accessToken: token.accessToken,\n user: user ? mapUserToAPI(user) : undefined,\n project: project ? mapProjectToAPI(project) : undefined,\n organization: organization ? mapOrganizationToAPI(organization) : undefined,\n grants,\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"oAuth2.service.mjs","names":[],"sources":["../../../src/services/oAuth2.service.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport type { Callback, Client } from '@node-oauth/oauth2-server';\nimport { OAuth2AccessTokenModel } from '@schemas/oAuth2.schema';\nimport { ProjectModel } from '@schemas/project.schema';\nimport { ensureMongoDocumentToObject } from '@utils/ensureMongoDocumentToObject';\nimport { GenericError } from '@utils/errors';\nimport { mapOrganizationToAPI } from '@utils/mapper/organization';\nimport { mapProjectToAPI } from '@utils/mapper/project';\nimport { mapUserToAPI } from '@utils/mapper/user';\nimport { getTokenExpireAt, shouldExtendOAuth2Token } from '@utils/oAuth2';\nimport type { Types } from 'mongoose';\nimport type { OAuth2Token } from '@/types/oAuth2.types';\nimport type { Organization } from '@/types/organization.types';\nimport type {\n OAuth2Access,\n OAuth2AccessContext,\n Project,\n ProjectDocument,\n} from '@/types/project.types';\nimport type { User, UserAPI, UserDocument } from '@/types/user.types';\nimport type { Token } from '../schemas/oAuth2.schema';\nimport { getOrganizationById } from './organization.service';\nimport { getUserById } from './user.service';\n\n/**\n * Function to generate client credentials\n *\n * @returns The client id and client secret\n */\nexport const generateClientCredentials = (): {\n clientId: string;\n clientSecret: string;\n} => {\n const clientId = randomBytes(16).toString('hex'); // Generate a 16 character hexadecimal string\n const clientSecret = randomBytes(32).toString('hex'); // Generate a 32 character hexadecimal string\n\n return { clientId, clientSecret };\n};\n\n/**\n * Method to get the client and the project\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The an object containing the client, the rights and the project or false if not found\n */\nexport const getClientAndProjectByClientId = async (\n clientId: string\n): Promise<\n | {\n client: Client;\n oAuth2Access: OAuth2Access;\n project: ProjectDocument;\n grants: Token['grants'];\n }\n | false\n> => {\n const project = await ProjectModel.findOne({\n 'oAuth2Access.clientId': String(clientId),\n });\n\n if (!project) {\n return false;\n }\n\n const oAuth2Access = project.oAuth2Access.find(\n (access) => access.clientId === clientId\n );\n\n if (!oAuth2Access) {\n return false;\n }\n\n const formattedClient: Client = {\n id: oAuth2Access.clientId,\n clientId,\n clientSecret: oAuth2Access.clientSecret,\n grants: ['client_credentials'],\n };\n\n return {\n client: formattedClient,\n oAuth2Access,\n grants: oAuth2Access.grants,\n project,\n };\n};\n\n/**\n * Get the client and verify that the client secret is correct\n *\n * @param clientId - The client id\n * @param clientSecret - The client secret\n * @returns The client or false if not found\n */\nexport const getClient = async (\n clientId: string,\n clientSecret: string\n): Promise<Client | false> => {\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client } = result;\n\n if (!client || client.clientSecret !== clientSecret) {\n return false;\n }\n\n return client;\n};\n\n/**\n * Format an OAuth2Token\n *\n * @param token - The token to format\n * @param client - The client\n * @param user - The user\n * @param project - The project\n * @param organization - The organization\n * @param grants - The grants\n * @returns The formatted token\n */\nexport const formatOAuth2Token = (\n token: Token,\n client: Client,\n user: UserAPI,\n project: Project,\n organization: Organization,\n grants: Token['grants']\n): OAuth2Token => {\n const { clientId, userId, ...restToken } = token;\n\n if (String(userId) !== String(user.id)) {\n throw new GenericError('USER_ID_MISMATCH');\n }\n\n const formattedToken: OAuth2Token = {\n ...restToken,\n client,\n user: mapUserToAPI(user),\n organization: mapOrganizationToAPI(organization),\n project: mapProjectToAPI(project),\n accessToken: token.accessToken,\n accessTokenExpiresAt: token.accessTokenExpiresAt ?? new Date('999-99-99'),\n grants,\n };\n\n return formattedToken;\n};\n\n/**\n * Format a auth token for the database\n *\n * @param token - The oAuth2 token to format\n * @param clientId - The client ID\n * @param userId - The user ID\n * @returns\n */\nexport const formatDBToken = (\n token: OAuth2Token,\n clientId: Client['id'],\n userId: User['id'] | string\n): Token => {\n const formattedToken: Token = {\n id: token.id,\n clientId: clientId,\n userId: userId as Types.ObjectId,\n accessToken: token.accessToken,\n expiresIn: token.accessTokenExpiresAt ?? getTokenExpireAt(),\n };\n\n return formattedToken;\n};\n\n/**\n * Method to save the token\n *\n * @param token - The token\n * @param client - The client\n * @param user - The user\n * @returns The saved token or false if not saved\n */\nexport const saveToken = async (\n token: OAuth2Token,\n client: Client,\n user: UserAPI\n): Promise<OAuth2Token | false> => {\n const formattedAccessToken: Token = formatDBToken(token, client.id, user.id);\n\n const result = await OAuth2AccessTokenModel.create(formattedAccessToken);\n\n if (!result) {\n return false;\n }\n\n const result2 = await getClientAndProjectByClientId(result.clientId);\n\n if (!result2) {\n return false;\n }\n\n const { project } = result2;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedResult = formatOAuth2Token(\n formattedAccessToken,\n client,\n user,\n project,\n organization,\n token.rights\n );\n return formattedResult;\n};\n\n/**\n * Sliding-refresh: push the token's expiry forward when it has been used\n * within the refresh threshold. Idempotent and cheap when no extension is due.\n */\nexport const extendOAuth2AccessToken = async (\n accessToken: string\n): Promise<Date> => {\n const nextExpiresAt = getTokenExpireAt();\n await OAuth2AccessTokenModel.updateOne(\n { accessToken: String(accessToken) },\n { $set: { accessTokenExpiresAt: nextExpiresAt, expiresIn: nextExpiresAt } }\n );\n return nextExpiresAt;\n};\n\n/**\n * Method to get the access token\n *\n * @param accessToken - The access token\n * @returns The access token or false if not found\n */\nexport const getAccessToken = async (\n accessToken: string\n): Promise<OAuth2Token | false> => {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken: String(accessToken),\n });\n\n if (!token) {\n return false;\n }\n\n // Slide the expiry forward when this active token is approaching its\n // deadline so a long-lived integration doesn't have to re-authenticate.\n const currentExpiresAt = token.accessTokenExpiresAt ?? token.expiresIn;\n if (currentExpiresAt && shouldExtendOAuth2Token(currentExpiresAt)) {\n const nextExpiresAt = await extendOAuth2AccessToken(accessToken);\n token.accessTokenExpiresAt = nextExpiresAt;\n token.expiresIn = nextExpiresAt;\n }\n\n const { userId, clientId } = token;\n\n const user = await getUserById(userId);\n\n if (!user) {\n return false;\n }\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n return false;\n }\n\n const { client, project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n if (!organization) {\n return false;\n }\n\n const formattedAccessToken = formatOAuth2Token(\n token,\n client,\n user,\n project,\n organization,\n grants\n );\n\n return formattedAccessToken;\n};\n\n/**\n * Method to get the user from the client\n *\n * @param client - The client\n * @returns The user or false if not found\n */\nexport const getUserFromClient = async (\n client: Client\n): Promise<UserDocument | false> => {\n const response = await getClientAndProjectByClientId(client.id);\n\n if (!response) {\n return false;\n }\n\n const { userId } = response.oAuth2Access;\n\n if (!userId) {\n return false;\n }\n\n const user = await getUserById(userId);\n\n return user ?? false;\n};\n\n/**\n * Method to verify the permissions (grants)\n *\n * @param token - The token\n * @param scope - The scope\n * @returns True if the token has the required scope, false otherwise\n */\nexport const verifyScope = async (\n _token: OAuth2Token,\n _scope: string,\n _callback?: Callback<boolean> | undefined\n): Promise<boolean> => {\n // Implement the verification of scopes if necessary\n return true;\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const validateOAuth2AccessToken = async (\n accessToken: string\n): Promise<Token> => {\n try {\n const token = await OAuth2AccessTokenModel.findOne({\n accessToken,\n });\n\n if (!token) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n // Check if token is expired\n if (new Date() > new Date(token.expiresIn)) {\n throw new GenericError('EXPIRED_ACCESS_TOKEN');\n }\n\n return ensureMongoDocumentToObject(token);\n } catch (_error) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n};\n\n/**\n * Validate OAuth2 access token and return user context\n */\nexport const getOAuth2AccessTokenContext = async (\n token: Token\n): Promise<OAuth2AccessContext> => {\n const { userId, clientId } = token;\n\n const user = await getUserById(String(userId));\n\n const result = await getClientAndProjectByClientId(clientId);\n\n if (!result) {\n throw new GenericError('INVALID_ACCESS_TOKEN');\n }\n\n const { project, grants } = result;\n\n const organization = await getOrganizationById(project.organizationId);\n\n return {\n accessToken: token.accessToken,\n user: user ? mapUserToAPI(user) : undefined,\n project: project ? mapProjectToAPI(project) : undefined,\n organization: organization ? mapOrganizationToAPI(organization) : undefined,\n grants,\n };\n};\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { GenericError } from "../utils/errors/ErrorsClass.mjs";
2
2
  import { validateOrganization } from "../utils/validation/validateOrganization.mjs";
3
- import { OrganizationModel } from "../models/organization.model.mjs";
3
+ import { OrganizationModel } from "../schemas/organization.schema.mjs";
4
4
 
5
5
  //#region src/services/organization.service.ts
6
6
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"organization.service.mjs","names":[],"sources":["../../../src/services/organization.service.ts"],"sourcesContent":["import { OrganizationModel } from '@models/organization.model';\nimport { GenericError } from '@utils/errors';\nimport type { OrganizationFilters } from '@utils/filtersAndPagination/getOrganizationFiltersAndPagination';\nimport {\n type OrganizationFields,\n validateOrganization,\n} from '@utils/validation/validateOrganization';\nimport type { Types } from 'mongoose';\nimport type {\n Organization,\n OrganizationAPI,\n OrganizationCreationData,\n OrganizationDocument,\n} from '@/types/organization.types';\nimport type { Plan, PlanDocument } from '@/types/plan.types';\n\n/**\n * Finds organizations 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 organizations matching the filters.\n */\nexport const findOrganizations = async (\n filters: OrganizationFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<OrganizationDocument[]> => {\n let query = OrganizationModel.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 an organization by its ID.\n * @param organizationId - The ID of the organization to find.\n * @returns The organization matching the ID.\n */\nexport const getOrganizationById = async (\n organizationId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const organization = await OrganizationModel.findById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Counts the total number of organizations that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of organizations.\n */\nexport const countOrganizations = async (\n filters: OrganizationFilters\n): Promise<number> => {\n const result = await OrganizationModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('ORGANIZATION_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new organization in the database.\n * @param organization - The organization data to create.\n * @returns The created organization.\n */\nexport const createOrganization = async (\n organization: OrganizationCreationData,\n userId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const errors = validateOrganization(organization, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', { errors });\n }\n\n try {\n const result = await OrganizationModel.create({\n creatorId: userId,\n membersIds: [userId],\n adminsIds: [userId],\n ...organization,\n });\n\n return result;\n } catch (error) {\n throw new GenericError('ORGANIZATION_CREATION_FAILED', {\n error: (error as Error).message,\n });\n }\n};\n\n/**\n * Updates an existing organization in the database by its ID.\n * @param organizationId - The ID of the organization to update.\n * @param organization - The updated organization data.\n * @returns The updated organization.\n */\nexport const updateOrganizationById = async (\n organizationId: string | Types.ObjectId,\n organization: Partial<Organization | OrganizationAPI>\n): Promise<OrganizationDocument> => {\n const updatedKeys = Object.keys(organization) as OrganizationFields;\n const errors = validateOrganization(organization, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', {\n organizationId,\n errors,\n });\n }\n\n const result = await OrganizationModel.updateOne(\n { _id: organizationId },\n organization\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', { organizationId });\n }\n\n return await getOrganizationById(organizationId);\n};\n\n/**\n * Deletes an organization from the database by its ID.\n * @param organizationId - The ID of the organization to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteOrganizationById = async (\n organizationId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const organization =\n await OrganizationModel.findByIdAndDelete(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Updates an existing plan in the database by its ID.\n * @param planId - The ID of the plan to update.\n * @param plan - The updated plan data.\n * @returns The updated plan.\n */\nexport const updatePlan = async (\n organization: Organization | OrganizationDocument,\n plan: Partial<Plan>\n): Promise<OrganizationDocument | null> => {\n let prevPlan = organization.plan ?? {};\n\n if (typeof (prevPlan as PlanDocument)?.toObject === 'function') {\n prevPlan = (prevPlan as PlanDocument).toObject();\n }\n\n const updateOrganizationResult = await OrganizationModel.updateOne(\n { _id: organization.id },\n { $set: { plan: { ...prevPlan, ...plan } } }\n );\n\n if (updateOrganizationResult.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await getOrganizationById(organization.id);\n\n return updatedOrganization;\n};\n"],"mappings":";;;;;;;;;;;;AAuBA,MAAa,oBAAoB,OAC/B,SACA,MACA,OACA,gBACoC;CACpC,IAAI,QAAQ,kBAAkB,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,KAAK;CAElE,IAAI,eAAe,OAAO,KAAK,WAAW,EAAE,SAAS,GACnD,QAAQ,MAAM,KAAK,WAAW;CAGhC,OAAO,MAAM;AACf;;;;;;AAOA,MAAa,sBAAsB,OACjC,mBACkC;CAClC,MAAM,eAAe,MAAM,kBAAkB,SAAS,cAAc;CAEpE,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;CAGrE,OAAO;AACT;;;;;;AAOA,MAAa,qBAAqB,OAChC,YACoB;CACpB,MAAM,SAAS,MAAM,kBAAkB,eAAe,OAAO;CAE7D,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,6BAA6B,EAAE,QAAQ,CAAC;CAGjE,OAAO;AACT;;;;;;AAOA,MAAa,qBAAqB,OAChC,cACA,WACkC;CAClC,MAAM,SAAS,qBAAqB,cAAc,CAAC,MAAM,CAAC;CAE1D,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,+BAA+B,EAAE,OAAO,CAAC;CAGlE,IAAI;EAQF,OAAO,MAPc,kBAAkB,OAAO;GAC5C,WAAW;GACX,YAAY,CAAC,MAAM;GACnB,WAAW,CAAC,MAAM;GAClB,GAAG;EACL,CAAC;CAGH,SAAS,OAAO;EACd,MAAM,IAAI,aAAa,gCAAgC,EACrD,OAAQ,MAAgB,QAC1B,CAAC;CACH;AACF;;;;;;;AAQA,MAAa,yBAAyB,OACpC,gBACA,iBACkC;CAElC,MAAM,SAAS,qBAAqB,cADhB,OAAO,KAAK,YAC4B,CAAC;CAE7D,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,+BAA+B;EACpD;EACA;CACF,CAAC;CAQH,KAAI,MALiB,kBAAkB,UACrC,EAAE,KAAK,eAAe,GACtB,YACF,GAEW,iBAAiB,GAC1B,MAAM,IAAI,aAAa,8BAA8B,EAAE,eAAe,CAAC;CAGzE,OAAO,MAAM,oBAAoB,cAAc;AACjD;;;;;;AAOA,MAAa,yBAAyB,OACpC,mBACkC;CAClC,MAAM,eACJ,MAAM,kBAAkB,kBAAkB,cAAc;CAE1D,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;CAGrE,OAAO;AACT;;;;;;;AAQA,MAAa,aAAa,OACxB,cACA,SACyC;CACzC,IAAI,WAAW,aAAa,QAAQ,CAAC;CAErC,IAAI,OAAQ,UAA2B,aAAa,YAClD,WAAY,SAA0B,SAAS;CAQjD,KAAI,MALmC,kBAAkB,UACvD,EAAE,KAAK,aAAa,GAAG,GACvB,EAAE,MAAM,EAAE,MAAM;EAAE,GAAG;EAAU,GAAG;CAAK,EAAE,EAAE,CAC7C,GAE6B,iBAAiB,GAC5C,MAAM,IAAI,aAAa,8BAA8B,EACnD,gBAAgB,aAAa,GAC/B,CAAC;CAKH,OAAO,MAF2B,oBAAoB,aAAa,EAAE;AAGvE"}
1
+ {"version":3,"file":"organization.service.mjs","names":[],"sources":["../../../src/services/organization.service.ts"],"sourcesContent":["import { OrganizationModel } from '@schemas/organization.schema';\nimport { GenericError } from '@utils/errors';\nimport type { OrganizationFilters } from '@utils/filtersAndPagination/getOrganizationFiltersAndPagination';\nimport {\n type OrganizationFields,\n validateOrganization,\n} from '@utils/validation/validateOrganization';\nimport type { Types } from 'mongoose';\nimport type {\n Organization,\n OrganizationAPI,\n OrganizationCreationData,\n OrganizationDocument,\n} from '@/types/organization.types';\nimport type { Plan, PlanDocument } from '@/types/plan.types';\n\n/**\n * Finds organizations 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 organizations matching the filters.\n */\nexport const findOrganizations = async (\n filters: OrganizationFilters,\n skip: number,\n limit: number,\n sortOptions?: Record<string, 1 | -1>\n): Promise<OrganizationDocument[]> => {\n let query = OrganizationModel.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 an organization by its ID.\n * @param organizationId - The ID of the organization to find.\n * @returns The organization matching the ID.\n */\nexport const getOrganizationById = async (\n organizationId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const organization = await OrganizationModel.findById(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Counts the total number of organizations that match the filters.\n * @param filters - MongoDB filter query.\n * @returns Total number of organizations.\n */\nexport const countOrganizations = async (\n filters: OrganizationFilters\n): Promise<number> => {\n const result = await OrganizationModel.countDocuments(filters);\n\n if (typeof result === 'undefined') {\n throw new GenericError('ORGANIZATION_COUNT_FAILED', { filters });\n }\n\n return result;\n};\n\n/**\n * Creates a new organization in the database.\n * @param organization - The organization data to create.\n * @returns The created organization.\n */\nexport const createOrganization = async (\n organization: OrganizationCreationData,\n userId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const errors = validateOrganization(organization, ['name']);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', { errors });\n }\n\n try {\n const result = await OrganizationModel.create({\n creatorId: userId,\n membersIds: [userId],\n adminsIds: [userId],\n ...organization,\n });\n\n return result;\n } catch (error) {\n throw new GenericError('ORGANIZATION_CREATION_FAILED', {\n error: (error as Error).message,\n });\n }\n};\n\n/**\n * Updates an existing organization in the database by its ID.\n * @param organizationId - The ID of the organization to update.\n * @param organization - The updated organization data.\n * @returns The updated organization.\n */\nexport const updateOrganizationById = async (\n organizationId: string | Types.ObjectId,\n organization: Partial<Organization | OrganizationAPI>\n): Promise<OrganizationDocument> => {\n const updatedKeys = Object.keys(organization) as OrganizationFields;\n const errors = validateOrganization(organization, updatedKeys);\n\n if (Object.keys(errors).length > 0) {\n throw new GenericError('ORGANIZATION_INVALID_FIELDS', {\n organizationId,\n errors,\n });\n }\n\n const result = await OrganizationModel.updateOne(\n { _id: organizationId },\n organization\n );\n\n if (result.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', { organizationId });\n }\n\n return await getOrganizationById(organizationId);\n};\n\n/**\n * Deletes an organization from the database by its ID.\n * @param organizationId - The ID of the organization to delete.\n * @returns The result of the deletion operation.\n */\nexport const deleteOrganizationById = async (\n organizationId: string | Types.ObjectId\n): Promise<OrganizationDocument> => {\n const organization =\n await OrganizationModel.findByIdAndDelete(organizationId);\n\n if (!organization) {\n throw new GenericError('ORGANIZATION_NOT_FOUND', { organizationId });\n }\n\n return organization;\n};\n\n/**\n * Updates an existing plan in the database by its ID.\n * @param planId - The ID of the plan to update.\n * @param plan - The updated plan data.\n * @returns The updated plan.\n */\nexport const updatePlan = async (\n organization: Organization | OrganizationDocument,\n plan: Partial<Plan>\n): Promise<OrganizationDocument | null> => {\n let prevPlan = organization.plan ?? {};\n\n if (typeof (prevPlan as PlanDocument)?.toObject === 'function') {\n prevPlan = (prevPlan as PlanDocument).toObject();\n }\n\n const updateOrganizationResult = await OrganizationModel.updateOne(\n { _id: organization.id },\n { $set: { plan: { ...prevPlan, ...plan } } }\n );\n\n if (updateOrganizationResult.matchedCount === 0) {\n throw new GenericError('ORGANIZATION_UPDATE_FAILED', {\n organizationId: organization.id,\n });\n }\n\n const updatedOrganization = await getOrganizationById(organization.id);\n\n return updatedOrganization;\n};\n"],"mappings":";;;;;;;;;;;;AAuBA,MAAa,oBAAoB,OAC/B,SACA,MACA,OACA,gBACoC;CACpC,IAAI,QAAQ,kBAAkB,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,KAAK;CAElE,IAAI,eAAe,OAAO,KAAK,WAAW,EAAE,SAAS,GACnD,QAAQ,MAAM,KAAK,WAAW;CAGhC,OAAO,MAAM;AACf;;;;;;AAOA,MAAa,sBAAsB,OACjC,mBACkC;CAClC,MAAM,eAAe,MAAM,kBAAkB,SAAS,cAAc;CAEpE,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;CAGrE,OAAO;AACT;;;;;;AAOA,MAAa,qBAAqB,OAChC,YACoB;CACpB,MAAM,SAAS,MAAM,kBAAkB,eAAe,OAAO;CAE7D,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,6BAA6B,EAAE,QAAQ,CAAC;CAGjE,OAAO;AACT;;;;;;AAOA,MAAa,qBAAqB,OAChC,cACA,WACkC;CAClC,MAAM,SAAS,qBAAqB,cAAc,CAAC,MAAM,CAAC;CAE1D,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,+BAA+B,EAAE,OAAO,CAAC;CAGlE,IAAI;EAQF,OAAO,MAPc,kBAAkB,OAAO;GAC5C,WAAW;GACX,YAAY,CAAC,MAAM;GACnB,WAAW,CAAC,MAAM;GAClB,GAAG;EACL,CAAC;CAGH,SAAS,OAAO;EACd,MAAM,IAAI,aAAa,gCAAgC,EACrD,OAAQ,MAAgB,QAC1B,CAAC;CACH;AACF;;;;;;;AAQA,MAAa,yBAAyB,OACpC,gBACA,iBACkC;CAElC,MAAM,SAAS,qBAAqB,cADhB,OAAO,KAAK,YAC4B,CAAC;CAE7D,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,+BAA+B;EACpD;EACA;CACF,CAAC;CAQH,KAAI,MALiB,kBAAkB,UACrC,EAAE,KAAK,eAAe,GACtB,YACF,GAEW,iBAAiB,GAC1B,MAAM,IAAI,aAAa,8BAA8B,EAAE,eAAe,CAAC;CAGzE,OAAO,MAAM,oBAAoB,cAAc;AACjD;;;;;;AAOA,MAAa,yBAAyB,OACpC,mBACkC;CAClC,MAAM,eACJ,MAAM,kBAAkB,kBAAkB,cAAc;CAE1D,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,0BAA0B,EAAE,eAAe,CAAC;CAGrE,OAAO;AACT;;;;;;;AAQA,MAAa,aAAa,OACxB,cACA,SACyC;CACzC,IAAI,WAAW,aAAa,QAAQ,CAAC;CAErC,IAAI,OAAQ,UAA2B,aAAa,YAClD,WAAY,SAA0B,SAAS;CAQjD,KAAI,MALmC,kBAAkB,UACvD,EAAE,KAAK,aAAa,GAAG,GACvB,EAAE,MAAM,EAAE,MAAM;EAAE,GAAG;EAAU,GAAG;CAAK,EAAE,EAAE,CAC7C,GAE6B,iBAAiB,GAC5C,MAAM,IAAI,aAAa,8BAA8B,EACnD,gBAAgB,aAAa,GAC/B,CAAC;CAKH,OAAO,MAF2B,oBAAoB,aAAa,EAAE;AAGvE"}
@@ -2,7 +2,7 @@ import { ensureMongoDocumentToObject } from "../utils/ensureMongoDocumentToObjec
2
2
  import { GenericError } from "../utils/errors/ErrorsClass.mjs";
3
3
  import { removeObjectKeys } from "../utils/removeObjectKeys.mjs";
4
4
  import { validateProject } from "../utils/validation/validateProject.mjs";
5
- import { ProjectModel } from "../models/project.model.mjs";
5
+ import { ProjectModel } from "../schemas/project.schema.mjs";
6
6
 
7
7
  //#region src/services/project.service.ts
8
8
  /**