@intlayer/backend 8.10.0 → 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.
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +10948 -8936
- package/dist/assets/utils/AI/askDocQuestion/embeddings/docs/en/interest_of_intlayer.json +1 -14957
- package/dist/esm/controllers/ai.controller.mjs +10 -9
- package/dist/esm/controllers/ai.controller.mjs.map +1 -1
- package/dist/esm/controllers/audit.controller.mjs +1 -1
- package/dist/esm/controllers/audit.controller.mjs.map +1 -1
- package/dist/esm/controllers/organization.controller.mjs +1 -1
- package/dist/esm/controllers/organization.controller.mjs.map +1 -1
- package/dist/esm/controllers/project.controller.mjs +2 -2
- package/dist/esm/controllers/project.controller.mjs.map +1 -1
- package/dist/esm/controllers/showcaseProject.controller.mjs +0 -2
- package/dist/esm/controllers/showcaseProject.controller.mjs.map +1 -1
- package/dist/esm/controllers/translation.controller.mjs +0 -1
- package/dist/esm/controllers/translation.controller.mjs.map +1 -1
- package/dist/esm/controllers/user.controller.mjs +0 -6
- package/dist/esm/controllers/user.controller.mjs.map +1 -1
- package/dist/esm/schemas/account.schema.mjs +3 -2
- package/dist/esm/schemas/account.schema.mjs.map +1 -1
- package/dist/esm/{models/audit.model.mjs → schemas/audit.schema.mjs} +3 -3
- package/dist/esm/schemas/audit.schema.mjs.map +1 -0
- package/dist/esm/{models/auditJob.model.mjs → schemas/auditJob.schema.mjs} +3 -3
- package/dist/esm/schemas/auditJob.schema.mjs.map +1 -0
- package/dist/esm/{models/auditPage.model.mjs → schemas/auditPage.schema.mjs} +3 -3
- package/dist/esm/schemas/auditPage.schema.mjs.map +1 -0
- package/dist/esm/schemas/cliSessionToken.schema.mjs +3 -2
- package/dist/esm/schemas/cliSessionToken.schema.mjs.map +1 -1
- package/dist/esm/schemas/dictionary.schema.mjs +3 -2
- package/dist/esm/schemas/dictionary.schema.mjs.map +1 -1
- package/dist/esm/schemas/discussion.schema.mjs +3 -2
- package/dist/esm/schemas/discussion.schema.mjs.map +1 -1
- package/dist/esm/schemas/oAuth2.schema.mjs +3 -2
- package/dist/esm/schemas/oAuth2.schema.mjs.map +1 -1
- package/dist/esm/schemas/organization.schema.mjs +3 -2
- package/dist/esm/schemas/organization.schema.mjs.map +1 -1
- package/dist/esm/schemas/project.schema.mjs +3 -2
- package/dist/esm/schemas/project.schema.mjs.map +1 -1
- package/dist/esm/schemas/session.schema.mjs +3 -2
- package/dist/esm/schemas/session.schema.mjs.map +1 -1
- package/dist/esm/schemas/showcaseProject.schema.mjs +3 -2
- package/dist/esm/schemas/showcaseProject.schema.mjs.map +1 -1
- package/dist/esm/schemas/tag.schema.mjs +3 -2
- package/dist/esm/schemas/tag.schema.mjs.map +1 -1
- package/dist/esm/schemas/user.schema.mjs +3 -2
- package/dist/esm/schemas/user.schema.mjs.map +1 -1
- package/dist/esm/services/audit/recursiveAudit.service.mjs +2 -2
- package/dist/esm/services/audit/recursiveAudit.service.mjs.map +1 -1
- package/dist/esm/services/bitbucket.service.mjs +1 -1
- package/dist/esm/services/bitbucket.service.mjs.map +1 -1
- package/dist/esm/services/cliSessionToken.service.mjs +1 -1
- package/dist/esm/services/cliSessionToken.service.mjs.map +1 -1
- package/dist/esm/services/dictionary.service.mjs +1 -1
- package/dist/esm/services/dictionary.service.mjs.map +1 -1
- package/dist/esm/services/github.service.mjs +1 -1
- package/dist/esm/services/github.service.mjs.map +1 -1
- package/dist/esm/services/gitlab.service.mjs +1 -1
- package/dist/esm/services/gitlab.service.mjs.map +1 -1
- package/dist/esm/services/oAuth2.service.mjs +2 -2
- package/dist/esm/services/oAuth2.service.mjs.map +1 -1
- package/dist/esm/services/organization.service.mjs +1 -1
- package/dist/esm/services/organization.service.mjs.map +1 -1
- package/dist/esm/services/project.service.mjs +1 -1
- package/dist/esm/services/project.service.mjs.map +1 -1
- package/dist/esm/services/projectAccessKey.service.mjs +1 -1
- package/dist/esm/services/projectAccessKey.service.mjs.map +1 -1
- package/dist/esm/services/showcase/showcaseProject.service.mjs +1 -1
- package/dist/esm/services/showcase/showcaseProject.service.mjs.map +1 -1
- package/dist/esm/services/tag.service.mjs +1 -1
- package/dist/esm/services/tag.service.mjs.map +1 -1
- package/dist/esm/services/user.service.mjs +1 -1
- package/dist/esm/services/user.service.mjs.map +1 -1
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/dictionary/markdown.json +10948 -8936
- package/dist/esm/utils/AI/askDocQuestion/embeddings/docs/en/interest_of_intlayer.json +1 -14957
- package/dist/esm/utils/AI/auditDictionaryField/index.mjs +9 -0
- package/dist/esm/utils/AI/auditDictionaryField/index.mjs.map +1 -1
- package/dist/esm/utils/AI/getProjectAIOptions.mjs +20 -0
- package/dist/esm/utils/AI/getProjectAIOptions.mjs.map +1 -0
- package/dist/esm/utils/errors/ErrorHandler.mjs +40 -8
- package/dist/esm/utils/errors/ErrorHandler.mjs.map +1 -1
- package/dist/esm/utils/mapper/project.mjs +7 -1
- package/dist/esm/utils/mapper/project.mjs.map +1 -1
- package/dist/esm/utils/mongoDB/connectDB.mjs +12 -12
- package/dist/esm/utils/mongoDB/connectDB.mjs.map +1 -1
- package/dist/types/controllers/ai.controller.d.ts.map +1 -1
- package/dist/types/controllers/project.controller.d.ts.map +1 -1
- package/dist/types/controllers/showcaseProject.controller.d.ts.map +1 -1
- package/dist/types/controllers/translation.controller.d.ts.map +1 -1
- package/dist/types/controllers/user.controller.d.ts.map +1 -1
- package/dist/types/schemas/account.schema.d.ts +3 -2
- package/dist/types/schemas/account.schema.d.ts.map +1 -1
- package/dist/types/schemas/audit.schema.d.ts +64 -0
- package/dist/types/schemas/audit.schema.d.ts.map +1 -0
- package/dist/types/schemas/auditJob.schema.d.ts +122 -0
- package/dist/types/schemas/auditJob.schema.d.ts.map +1 -0
- package/dist/types/schemas/auditPage.schema.d.ts +120 -0
- package/dist/types/schemas/auditPage.schema.d.ts.map +1 -0
- package/dist/types/schemas/cliSessionToken.schema.d.ts +10 -3
- package/dist/types/schemas/cliSessionToken.schema.d.ts.map +1 -1
- package/dist/types/schemas/dictionary.schema.d.ts +22 -13
- package/dist/types/schemas/dictionary.schema.d.ts.map +1 -1
- package/dist/types/schemas/discussion.schema.d.ts +19 -14
- package/dist/types/schemas/discussion.schema.d.ts.map +1 -1
- package/dist/types/schemas/oAuth2.schema.d.ts +13 -3
- package/dist/types/schemas/oAuth2.schema.d.ts.map +1 -1
- package/dist/types/schemas/organization.schema.d.ts +7 -6
- package/dist/types/schemas/organization.schema.d.ts.map +1 -1
- package/dist/types/schemas/plans.schema.d.ts +7 -7
- package/dist/types/schemas/project.schema.d.ts +7 -6
- package/dist/types/schemas/project.schema.d.ts.map +1 -1
- package/dist/types/schemas/session.schema.d.ts +11 -10
- package/dist/types/schemas/session.schema.d.ts.map +1 -1
- package/dist/types/schemas/showcaseProject.schema.d.ts +16 -15
- package/dist/types/schemas/showcaseProject.schema.d.ts.map +1 -1
- package/dist/types/schemas/tag.schema.d.ts +9 -8
- package/dist/types/schemas/tag.schema.d.ts.map +1 -1
- package/dist/types/schemas/user.schema.d.ts +8 -7
- package/dist/types/schemas/user.schema.d.ts.map +1 -1
- package/dist/types/services/audit/recursiveAudit.service.d.ts +2 -2
- package/dist/types/types/project.types.d.ts +2 -1
- package/dist/types/types/project.types.d.ts.map +1 -1
- package/dist/types/utils/AI/getProjectAIOptions.d.ts +15 -0
- package/dist/types/utils/AI/getProjectAIOptions.d.ts.map +1 -0
- package/dist/types/utils/errors/ErrorHandler.d.ts +4 -4
- package/dist/types/utils/errors/ErrorHandler.d.ts.map +1 -1
- package/dist/types/utils/filtersAndPagination/getTagFiltersAndPagination.d.ts +3 -3
- package/dist/types/utils/mapper/project.d.ts.map +1 -1
- package/package.json +5 -5
- package/dist/esm/models/account.model.mjs +0 -9
- package/dist/esm/models/account.model.mjs.map +0 -1
- package/dist/esm/models/audit.model.mjs.map +0 -1
- package/dist/esm/models/auditJob.model.mjs.map +0 -1
- package/dist/esm/models/auditPage.model.mjs.map +0 -1
- package/dist/esm/models/cliSessionToken.model.mjs +0 -9
- package/dist/esm/models/cliSessionToken.model.mjs.map +0 -1
- package/dist/esm/models/dictionary.model.mjs +0 -9
- package/dist/esm/models/dictionary.model.mjs.map +0 -1
- package/dist/esm/models/discussion.model.mjs +0 -9
- package/dist/esm/models/discussion.model.mjs.map +0 -1
- package/dist/esm/models/oAuth2.model.mjs +0 -9
- package/dist/esm/models/oAuth2.model.mjs.map +0 -1
- package/dist/esm/models/organization.model.mjs +0 -9
- package/dist/esm/models/organization.model.mjs.map +0 -1
- package/dist/esm/models/project.model.mjs +0 -9
- package/dist/esm/models/project.model.mjs.map +0 -1
- package/dist/esm/models/session.model.mjs +0 -9
- package/dist/esm/models/session.model.mjs.map +0 -1
- package/dist/esm/models/showcaseProject.model.mjs +0 -9
- package/dist/esm/models/showcaseProject.model.mjs.map +0 -1
- package/dist/esm/models/tag.model.mjs +0 -9
- package/dist/esm/models/tag.model.mjs.map +0 -1
- package/dist/esm/models/user.model.mjs +0 -9
- package/dist/esm/models/user.model.mjs.map +0 -1
- package/dist/types/models/account.model.d.ts +0 -7
- package/dist/types/models/account.model.d.ts.map +0 -1
- package/dist/types/models/audit.model.d.ts +0 -18
- package/dist/types/models/audit.model.d.ts.map +0 -1
- package/dist/types/models/auditJob.model.d.ts +0 -31
- package/dist/types/models/auditJob.model.d.ts.map +0 -1
- package/dist/types/models/auditPage.model.d.ts +0 -29
- package/dist/types/models/auditPage.model.d.ts.map +0 -1
- package/dist/types/models/cliSessionToken.model.d.ts +0 -14
- package/dist/types/models/cliSessionToken.model.d.ts.map +0 -1
- package/dist/types/models/dictionary.model.d.ts +0 -16
- package/dist/types/models/dictionary.model.d.ts.map +0 -1
- package/dist/types/models/discussion.model.d.ts +0 -12
- package/dist/types/models/discussion.model.d.ts.map +0 -1
- package/dist/types/models/oAuth2.model.d.ts +0 -18
- package/dist/types/models/oAuth2.model.d.ts.map +0 -1
- package/dist/types/models/organization.model.d.ts +0 -7
- package/dist/types/models/organization.model.d.ts.map +0 -1
- package/dist/types/models/project.model.d.ts +0 -7
- package/dist/types/models/project.model.d.ts.map +0 -1
- package/dist/types/models/session.model.d.ts +0 -7
- package/dist/types/models/session.model.d.ts.map +0 -1
- package/dist/types/models/showcaseProject.model.d.ts +0 -7
- package/dist/types/models/showcaseProject.model.d.ts.map +0 -1
- package/dist/types/models/tag.model.d.ts +0 -7
- package/dist/types/models/tag.model.d.ts.map +0 -1
- package/dist/types/models/user.model.d.ts +0 -7
- package/dist/types/models/user.model.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"showcaseProject.schema.mjs","names":[],"sources":["../../../src/schemas/showcaseProject.schema.ts"],"sourcesContent":["import { Schema } from 'mongoose';\nimport type {
|
|
1
|
+
{"version":3,"file":"showcaseProject.schema.mjs","names":[],"sources":["../../../src/schemas/showcaseProject.schema.ts"],"sourcesContent":["import { model, Schema } from 'mongoose';\nimport type {\n ShowcaseProjectDocument,\n ShowcaseProjectModelType,\n} from '@/types/showcaseProject.types';\n\nconst scanDetailsSchema = new Schema(\n {\n score: { type: Number },\n langTag: { type: String },\n htmlDir: { type: String },\n hreflangs: { type: [String] },\n hasXDefault: { type: Boolean },\n hasCanonical: { type: Boolean },\n hasLocalizedLinks: { type: Boolean },\n allAnchorsLocalized: { type: Boolean },\n robotsTxt: {\n accessible: { type: Boolean },\n disallowWithoutLocaleAlternates: { type: Boolean },\n },\n sitemapXml: {\n urlsDiscoveredCount: { type: Number },\n alternatesPresent: { type: Boolean },\n xDefaultPresent: { type: Boolean },\n },\n },\n { _id: false }\n);\n\nexport const showcaseProjectSchema = new Schema<ShowcaseProjectDocument>(\n {\n title: { type: String, required: true },\n description: { type: String, default: '' },\n imageUrl: { type: String, default: '' },\n logoUrl: { type: String },\n websiteUrl: { type: String, required: true, unique: true },\n githubUrl: { type: String },\n tags: { type: [String], default: [] },\n upvoters: { type: [String], default: [] },\n downvoters: { type: [String], default: [] },\n isOpenSource: { type: Boolean, default: false },\n createdAt: { type: Date, default: Date.now },\n intlayerVersion: { type: String },\n libsUsed: { type: [String], default: [] },\n packageDetails: { type: Map, of: String, default: {} },\n lastScanDate: { type: Date },\n scanDetails: { type: scanDetailsSchema },\n owner: { type: String },\n status: {\n type: String,\n enum: ['pending_scan', 'active', 'scan_failed'],\n default: 'pending_scan',\n },\n },\n {\n timestamps: false,\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 ShowcaseProjectModel = model<\n ShowcaseProjectDocument,\n ShowcaseProjectModelType\n>('ShowcaseProject', showcaseProjectSchema);\n"],"mappings":";;;AAMA,MAAM,oBAAoB,IAAI,OAC5B;CACE,OAAO,EAAE,MAAM,OAAO;CACtB,SAAS,EAAE,MAAM,OAAO;CACxB,SAAS,EAAE,MAAM,OAAO;CACxB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;CAC5B,aAAa,EAAE,MAAM,QAAQ;CAC7B,cAAc,EAAE,MAAM,QAAQ;CAC9B,mBAAmB,EAAE,MAAM,QAAQ;CACnC,qBAAqB,EAAE,MAAM,QAAQ;CACrC,WAAW;EACT,YAAY,EAAE,MAAM,QAAQ;EAC5B,iCAAiC,EAAE,MAAM,QAAQ;CACnD;CACA,YAAY;EACV,qBAAqB,EAAE,MAAM,OAAO;EACpC,mBAAmB,EAAE,MAAM,QAAQ;EACnC,iBAAiB,EAAE,MAAM,QAAQ;CACnC;AACF,GACA,EAAE,KAAK,MAAM,CACf;AAEA,MAAa,wBAAwB,IAAI,OACvC;CACE,OAAO;EAAE,MAAM;EAAQ,UAAU;CAAK;CACtC,aAAa;EAAE,MAAM;EAAQ,SAAS;CAAG;CACzC,UAAU;EAAE,MAAM;EAAQ,SAAS;CAAG;CACtC,SAAS,EAAE,MAAM,OAAO;CACxB,YAAY;EAAE,MAAM;EAAQ,UAAU;EAAM,QAAQ;CAAK;CACzD,WAAW,EAAE,MAAM,OAAO;CAC1B,MAAM;EAAE,MAAM,CAAC,MAAM;EAAG,SAAS,CAAC;CAAE;CACpC,UAAU;EAAE,MAAM,CAAC,MAAM;EAAG,SAAS,CAAC;CAAE;CACxC,YAAY;EAAE,MAAM,CAAC,MAAM;EAAG,SAAS,CAAC;CAAE;CAC1C,cAAc;EAAE,MAAM;EAAS,SAAS;CAAM;CAC9C,WAAW;EAAE,MAAM;EAAM,SAAS,KAAK;CAAI;CAC3C,iBAAiB,EAAE,MAAM,OAAO;CAChC,UAAU;EAAE,MAAM,CAAC,MAAM;EAAG,SAAS,CAAC;CAAE;CACxC,gBAAgB;EAAE,MAAM;EAAK,IAAI;EAAQ,SAAS,CAAC;CAAE;CACrD,cAAc,EAAE,MAAM,KAAK;CAC3B,aAAa,EAAE,MAAM,kBAAkB;CACvC,OAAO,EAAE,MAAM,OAAO;CACtB,QAAQ;EACN,MAAM;EACN,MAAM;GAAC;GAAgB;GAAU;EAAa;EAC9C,SAAS;CACX;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,uBAAuB,MAGlC,mBAAmB,qBAAqB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KEY_MAX_LENGTH, KEY_MIN_LENGTH, NAME_MAX_LENGTH, NAME_MIN_LENGTH } from "../utils/validation/validateTag.mjs";
|
|
2
|
-
import { Schema } from "mongoose";
|
|
2
|
+
import { Schema, model } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/schemas/tag.schema.ts
|
|
5
5
|
const tagSchema = new Schema({
|
|
@@ -55,7 +55,8 @@ const tagSchema = new Schema({
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
|
+
const TagModel = model("tag", tagSchema);
|
|
58
59
|
|
|
59
60
|
//#endregion
|
|
60
|
-
export { tagSchema };
|
|
61
|
+
export { TagModel, tagSchema };
|
|
61
62
|
//# sourceMappingURL=tag.schema.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tag.schema.mjs","names":[],"sources":["../../../src/schemas/tag.schema.ts"],"sourcesContent":["import {\n KEY_MAX_LENGTH,\n KEY_MIN_LENGTH,\n NAME_MAX_LENGTH,\n NAME_MIN_LENGTH,\n} from '@utils/validation/validateTag';\nimport { Schema } from 'mongoose';\nimport type { TagSchema } from '@/types/tag.types';\n\nexport const tagSchema = new Schema<TagSchema>(\n {\n organizationId: {\n type: Schema.Types.ObjectId,\n ref: 'Organization',\n required: true,\n },\n projectId: {\n type: Schema.Types.ObjectId,\n ref: 'Project',\n required: true,\n },\n key: {\n type: String,\n required: true,\n minlength: KEY_MIN_LENGTH,\n maxlength: KEY_MAX_LENGTH,\n },\n name: {\n type: String,\n minlength: NAME_MIN_LENGTH,\n maxlength: NAME_MAX_LENGTH,\n },\n description: {\n type: String,\n },\n instructions: {\n type: String,\n },\n creatorId: {\n type: Schema.Types.ObjectId,\n ref: 'User',\n required: true,\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":";;;;AASA,MAAa,YAAY,IAAI,OAC3B;CACE,gBAAgB;EACd,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,UAAU;CACZ;CACA,WAAW;EACT,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,UAAU;CACZ;CACA,KAAK;EACH,MAAM;EACN,UAAU;EACV;EACA;CACF;CACA,MAAM;EACJ,MAAM;EACN;EACA;CACF;CACA,aAAa,EACX,MAAM,OACR;CACA,cAAc,EACZ,MAAM,OACR;CACA,WAAW;EACT,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,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":"tag.schema.mjs","names":[],"sources":["../../../src/schemas/tag.schema.ts"],"sourcesContent":["import {\n KEY_MAX_LENGTH,\n KEY_MIN_LENGTH,\n NAME_MAX_LENGTH,\n NAME_MIN_LENGTH,\n} from '@utils/validation/validateTag';\nimport { model, Schema } from 'mongoose';\nimport type { TagModelType, TagSchema } from '@/types/tag.types';\n\nexport const tagSchema = new Schema<TagSchema>(\n {\n organizationId: {\n type: Schema.Types.ObjectId,\n ref: 'Organization',\n required: true,\n },\n projectId: {\n type: Schema.Types.ObjectId,\n ref: 'Project',\n required: true,\n },\n key: {\n type: String,\n required: true,\n minlength: KEY_MIN_LENGTH,\n maxlength: KEY_MAX_LENGTH,\n },\n name: {\n type: String,\n minlength: NAME_MIN_LENGTH,\n maxlength: NAME_MAX_LENGTH,\n },\n description: {\n type: String,\n },\n instructions: {\n type: String,\n },\n creatorId: {\n type: Schema.Types.ObjectId,\n ref: 'User',\n required: true,\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 TagModel = model<TagSchema, TagModelType>('tag', tagSchema);\n"],"mappings":";;;;AASA,MAAa,YAAY,IAAI,OAC3B;CACE,gBAAgB;EACd,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,UAAU;CACZ;CACA,WAAW;EACT,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,UAAU;CACZ;CACA,KAAK;EACH,MAAM;EACN,UAAU;EACV;EACA;CACF;CACA,MAAM;EACJ,MAAM;EACN;EACA;CACF;CACA,aAAa,EACX,MAAM,OACR;CACA,cAAc,EACZ,MAAM,OACR;CACA,WAAW;EACT,MAAM,OAAO,MAAM;EACnB,KAAK;EACL,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,WAAW,MAA+B,OAAO,SAAS"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NAMES_MAX_LENGTH, NAMES_MIN_LENGTH } from "../utils/validation/validateUser.mjs";
|
|
2
|
-
import { Schema } from "mongoose";
|
|
2
|
+
import { Schema, model } from "mongoose";
|
|
3
3
|
import validator from "validator";
|
|
4
4
|
|
|
5
5
|
//#region src/schemas/user.schema.ts
|
|
@@ -92,7 +92,8 @@ const userSchema = new Schema({
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
});
|
|
95
|
+
const UserModel = model("user", userSchema);
|
|
95
96
|
|
|
96
97
|
//#endregion
|
|
97
|
-
export { userSchema };
|
|
98
|
+
export { UserModel, userSchema };
|
|
98
99
|
//# sourceMappingURL=user.schema.mjs.map
|
|
@@ -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 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"],"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"}
|
|
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 "../../
|
|
5
|
-
import { AuditPageModel, AuditPageStatus } from "../../
|
|
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 "../
|
|
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 "../
|
|
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 '@
|
|
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 "../
|
|
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":";;;;;;;;;;;;;;;;;;;AA4BA,MAAa,mBAAmB,OAC9B,SACA,OAAO,GACP,QAAQ,KACR,aACA,iBAAiB,SACiB;CAClC,IAAI;EAwBF,QAJyB,MAnBE,gBAAgB,UAA8B;GAEvE,EAAE,QAAQ,QAAQ;GAGlB,GAAI,eAAe,OAAO,KAAK,WAAW,EAAE,SAAS,IACjD,CAAC,EAAE,OAAO,YAAY,CAAC,IACvB,CAAC;GAGL,EAAE,OAAO,KAAK;GAGd,EAAE,QAAQ,MAAM;GAGhB,GAAI,CAAC,iBAAiB,CAAC,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC;EAC1D,CAAC,GAEqC,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;CACxB,SAAS,OAAO;EACd,OAAO,MAAM,gCAAgC,KAAK;EAClD,MAAM;CACR;AACF;;;;;;;;;;;AAYA,MAAa,oBAAoB,OAC/B,iBACgC;CAChC,MAAM,KAAK,MAAM,SAAS,QAAQ,YAAsB,IACpD,IAAI,MAAM,SAAS,YAAsB,IACzC;CAEJ,MAAM,eAAe,MAAM,gBAAgB,UAA8B,CAEvE,EAAE,QAAQ,EAAE,KAAK,GAAG,EAAE,GAGtB,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC;CAED,IAAI,CAAC,aAAa,QAChB,MAAM,IAAI,aAAa,wBAAwB,EAAE,aAAa,CAAC;CAGjE,OAAO,IAAI,gBAAgB,aAAa,EAAE;AAC5C;;;;;;AAOA,MAAa,qBAAqB,OAChC,eACA,cACgC;CAGhC,QAAO,MAFoB,sBAAsB,CAAC,aAAa,GAAG,SAAS,GAEvD;AACtB;AAEA,MAAa,wBAAwB,OACnC,gBACA,cACkC;CAClC,MAAM,eAAe,MAAM,gBAAgB,UAA8B,CAEvE,EAAE,QAAQ;EAAE,KAAK,EAAE,KAAK,eAAe;EAAG,YAAY;CAAU,EAAE,GAGlE,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC;CAED,IAAI,CAAC,cACH,MAAM,IAAI,aAAa,wBAAwB;EAC7C;EACA;CACF,CAAC;CAOH,OAJyB,aAAa,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;AACxB;AAEA,MAAa,wBAAwB,OACnC,MACA,cACkC;CA4BlC,QAJyB,MAvBE,gBAAgB,UAA8B,CAEvE,EACE,QAAQ;EACN,MAAM,EAAE,KAAK,KAAK;EAClB,YAAY;CACd,EACF,GAGA,EACE,YAAY,EACV,UAAU,EACR,MAAM;EACJ,OAAO,EAAE,gBAAgB,WAAW;EACpC,IAAI;EACJ,IAAI;CACN,EACF,EACF,EACF,CACF,CAAC,GAEqC,KACnC,WAAW,IAAI,gBAAgB,MAAM,CAGlB;AACxB;;;;;;AAOA,MAAa,oBAAoB,OAC/B,YACoB;CACpB,MAAM,SAAS,MAAM,gBAAgB,eAAe,OAAO;CAE3D,IAAI,OAAO,WAAW,aACpB,MAAM,IAAI,aAAa,2BAA2B,EAAE,QAAQ,CAAC;CAG/D,OAAO;AACT;;;;;;AAOA,MAAa,mBAAmB,OAC9B,eACgC;CAChC,MAAM,SAAS,MAAM,mBAAmB,UAAU;CAElD,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,6BAA6B,EAClD,OACF,CAAC;CAGH,OAAO,MAAM,gBAAgB,OAAO,UAAU;AAChD;;;;;;;AAQA,MAAa,uBAAuB,OAClC,cACA,eACgC;CAEhC,MAAM,qBAAqB,iBADF,4BAA4B,UACM,GAAG,CAC5D,IACF,CAAC;CAGD,MAAM,SAAS,MAAM,mBAAmB,oBADpB,OAAO,KAAK,kBACsC,CAAC;CAEvE,IAAI,OAAO,KAAK,MAAM,EAAE,SAAS,GAC/B,MAAM,IAAI,aAAa,6BAA6B;EAClD;EACA;CACF,CAAC;CAQH,KAAI,MALiB,gBAAgB,UACnC,EAAE,KAAK,aAAa,GACpB,kBACF,GAEW,iBAAiB,GAC1B,MAAM,IAAI,aAAa,4BAA4B,EAAE,aAAa,CAAC;CAKrE,OAAO,MAFyB,kBAAkB,YAAY;AAGhE;;;;;;;AAQA,MAAa,wBAAwB,OACnC,eACA,YACA,cACgC;CAChC,MAAM,WAAW,MAAM,gBAAgB,QAAQ;EAC7C,KAAK,OAAO,aAAa;EACzB,YAAY;CACd,CAAC;CAED,IAAI,CAAC,UACH,MAAM,IAAI,aAAa,4BAA4B,EAAE,cAAc,CAAC;CAItE,MAAM,qBAAqB,iBADF,4BAA4B,UACM,GAAG,CAC5D,IACF,CAAC;CAKD,OAAO,OAAO,UAAU,kBAAkB;CAK1C,SAAS,aAAa,SAAS;CAG/B,MAAM,SAAS,KAAK;CAEpB,OAAO;AACT;;;;;;AAOA,MAAa,uBAAuB,OAClC,iBACgC;CAChC,MAAM,aAAa,MAAM,gBAAgB,kBAAkB,YAAY;CAEvE,IAAI,CAAC,YACH,MAAM,IAAI,aAAa,wBAAwB,EAAE,aAAa,CAAC;CAGjE,OAAO;AACT;AAGA,MAAM,oBAAoB,YAA4B;CACpD,MAAM,QAAQ,QAAQ,MAAM,UAAU;CACtC,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,2BAA2B,SAAS;CAEtD,OAAO,SAAS,MAAM,IAAI,EAAE;AAC9B;AAEA,MAAa,oBAAoB,eAAmC;CAClE,MAAM,iBAAiB;CAEvB,MAAM,WAAW,CAAC,GAAI,WAAW,QAAQ,KAAK,KAAK,CAAC,CAAE;CACtD,MAAM,cAAc,SAAS,SAAS,SAAS;CAG/C,IAAI,YAAY,iBAAiB,WAAW,IAAI;CAChD,IAAI,aAAa,GAAG,iBAAiB;CAGrC,OAAO,SAAS,SAAS,UAAU,GAAG;EACpC,aAAa;EACb,aAAa,GAAG,iBAAiB;CACnC;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,yBAAyB,OACpC,YACA,cACkB;CAClB,MAAM,mBAAmB,oBAAoB,YAAY,SAAS;CAElE,KAAK,MAAM,cAAc,kBACvB,MAAM,iBAAiB,UAAU;AAErC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "../logger/index.mjs";
|
|
2
|
-
import { AccountModel } from "../
|
|
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 "../
|
|
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 "../
|
|
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 "../
|
|
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
|
|