@intranefr/superbackend 1.6.7 → 1.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
  2. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
  3. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
  4. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
  5. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
  6. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
  7. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
  8. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
  9. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
  10. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
  11. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
  12. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
  13. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
  14. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
  15. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
  16. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
  17. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
  18. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
  19. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
  20. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
  21. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
  22. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
  23. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
  24. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
  25. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
  26. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
  27. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
  28. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
  29. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
  30. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
  31. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
  32. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
  33. package/.beads/config.yaml +4 -0
  34. package/.beads/issues.jsonl +8 -0
  35. package/.beads/metadata.json +4 -0
  36. package/.env.example +8 -0
  37. package/autochangelog/.env.example +36 -0
  38. package/autochangelog/README.md +412 -0
  39. package/autochangelog/config/database.js +27 -0
  40. package/autochangelog/package.json +47 -0
  41. package/autochangelog/public/landing.html +581 -0
  42. package/autochangelog/server.js +104 -0
  43. package/autochangelog/src/app.js +181 -0
  44. package/autochangelog/src/config/database.js +26 -0
  45. package/autochangelog/src/controllers/auth.js +488 -0
  46. package/autochangelog/src/controllers/changelog.js +682 -0
  47. package/autochangelog/src/controllers/project.js +580 -0
  48. package/autochangelog/src/controllers/repository.js +780 -0
  49. package/autochangelog/src/middleware/auth.js +386 -0
  50. package/autochangelog/src/models/Changelog.js +443 -0
  51. package/autochangelog/src/models/Project.js +226 -0
  52. package/autochangelog/src/models/Repository.js +366 -0
  53. package/autochangelog/src/models/User.js +223 -0
  54. package/autochangelog/src/routes/auth.routes.js +32 -0
  55. package/autochangelog/src/routes/changelog.routes.js +42 -0
  56. package/autochangelog/src/routes/github-auth.routes.js +102 -0
  57. package/autochangelog/src/routes/project.routes.js +50 -0
  58. package/autochangelog/src/routes/repository.routes.js +54 -0
  59. package/autochangelog/src/services/changelog.js +722 -0
  60. package/autochangelog/src/services/github.js +243 -0
  61. package/autochangelog/utils/logger.js +77 -0
  62. package/autochangelog/views/404.ejs +18 -0
  63. package/autochangelog/views/dashboard.ejs +596 -0
  64. package/autochangelog/views/index.ejs +231 -0
  65. package/autochangelog/views/layouts/main.ejs +44 -0
  66. package/autochangelog/views/login.ejs +104 -0
  67. package/autochangelog/views/partials/footer.ejs +20 -0
  68. package/autochangelog/views/partials/navbar.ejs +51 -0
  69. package/autochangelog/views/register.ejs +109 -0
  70. package/autochangelog-cli/README.md +266 -0
  71. package/autochangelog-cli/bin/autochangelog +120 -0
  72. package/autochangelog-cli/package.json +46 -0
  73. package/autochangelog-cli/src/cli/commands/auth.js +291 -0
  74. package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
  75. package/autochangelog-cli/src/cli/commands/project.js +427 -0
  76. package/autochangelog-cli/src/cli/commands/repo.js +557 -0
  77. package/autochangelog-cli/src/cli/commands/stats.js +706 -0
  78. package/autochangelog-cli/src/cli/utils/config.js +277 -0
  79. package/autochangelog-cli/src/cli/utils/errors.js +307 -0
  80. package/autochangelog-cli/src/cli/utils/logger.js +75 -0
  81. package/autochangelog-cli/src/cli/utils/output.js +357 -0
  82. package/package.json +9 -3
  83. package/plugins/supercli/README.md +108 -0
  84. package/plugins/supercli/plugin.json +123 -0
  85. package/server.js +1 -1
  86. package/src/cli/api.js +380 -0
  87. package/src/cli/direct/agent-utils.js +61 -0
  88. package/src/cli/direct/cli-utils.js +112 -0
  89. package/src/cli/direct/data-seeding.js +307 -0
  90. package/src/cli/direct/db-admin.js +84 -0
  91. package/src/cli/direct/db-advanced.js +372 -0
  92. package/src/cli/direct/db-utils.js +558 -0
  93. package/src/cli/direct/help.js +195 -0
  94. package/src/cli/direct/migration.js +107 -0
  95. package/src/cli/direct/rbac-advanced.js +132 -0
  96. package/src/cli/direct/resources-additional.js +400 -0
  97. package/src/cli/direct/resources-cms-advanced.js +173 -0
  98. package/src/cli/direct/resources-cms.js +247 -0
  99. package/src/cli/direct/resources-core.js +253 -0
  100. package/src/cli/direct/resources-execution.js +367 -0
  101. package/src/cli/direct/resources-health.js +152 -0
  102. package/src/cli/direct/resources-integrations.js +182 -0
  103. package/src/cli/direct/resources-logs.js +204 -0
  104. package/src/cli/direct/resources-org-rbac.js +187 -0
  105. package/src/cli/direct/resources-system.js +236 -0
  106. package/src/cli/direct.js +556 -0
  107. package/src/controllers/admin.controller.js +4 -0
  108. package/src/controllers/auth.controller.js +148 -1
  109. package/src/controllers/waitingList.controller.js +130 -1
  110. package/src/models/RbacRole.js +1 -1
  111. package/src/models/User.js +39 -5
  112. package/src/routes/auth.routes.js +6 -0
  113. package/src/routes/waitingList.routes.js +12 -2
  114. package/src/routes/waitingListAdmin.routes.js +3 -0
  115. package/src/services/email.service.js +1 -0
  116. package/src/services/github.service.js +255 -0
  117. package/src/services/rateLimiter.service.js +29 -1
  118. package/src/services/waitingListJson.service.js +32 -3
  119. package/views/admin-waiting-list.ejs +386 -3
@@ -0,0 +1,443 @@
1
+ const mongoose = require("mongoose");
2
+
3
+ const changelogSchema = new mongoose.Schema(
4
+ {
5
+ projectId: {
6
+ type: mongoose.Schema.Types.ObjectId,
7
+ ref: "Project",
8
+ required: true,
9
+ },
10
+ repositoryId: {
11
+ type: mongoose.Schema.Types.ObjectId,
12
+ ref: "Repository",
13
+ required: true,
14
+ },
15
+
16
+ // Changelog Information
17
+ title: {
18
+ type: String,
19
+ required: true,
20
+ trim: true,
21
+ maxlength: [200, "Title cannot exceed 200 characters"],
22
+ },
23
+ description: {
24
+ type: String,
25
+ trim: true,
26
+ maxlength: [500, "Description cannot exceed 500 characters"],
27
+ },
28
+
29
+ // Time Period
30
+ month: {
31
+ type: String,
32
+ required: true,
33
+ enum: [
34
+ "01",
35
+ "02",
36
+ "03",
37
+ "04",
38
+ "05",
39
+ "06",
40
+ "07",
41
+ "08",
42
+ "09",
43
+ "10",
44
+ "11",
45
+ "12",
46
+ ],
47
+ },
48
+ year: {
49
+ type: Number,
50
+ required: true,
51
+ min: [2000, "Year must be 2000 or later"],
52
+ max: [2100, "Year must be 2100 or earlier"],
53
+ index: true,
54
+ },
55
+
56
+ // Content
57
+ content: {
58
+ type: String,
59
+ required: true,
60
+ },
61
+
62
+ // Format and Storage
63
+ format: {
64
+ type: String,
65
+ enum: ["markdown", "html", "json"],
66
+ default: "markdown",
67
+ },
68
+ downloadUrl: {
69
+ type: String,
70
+ trim: true,
71
+ },
72
+ fileSize: {
73
+ type: Number,
74
+ default: 0,
75
+ },
76
+
77
+ // Generation Status
78
+ status: {
79
+ type: String,
80
+ enum: ["pending", "generating", "completed", "failed"],
81
+ default: "pending",
82
+ },
83
+ error: {
84
+ type: String,
85
+ maxlength: [1000, "Error message cannot exceed 1000 characters"],
86
+ },
87
+
88
+ // Generation Metadata
89
+ generatedAt: {
90
+ type: Date,
91
+ },
92
+ generatedBy: {
93
+ type: mongoose.Schema.Types.ObjectId,
94
+ ref: "users", // SuperBackend User model
95
+ },
96
+ generationTime: {
97
+ type: Number, // in milliseconds
98
+ },
99
+
100
+ // Content Metadata
101
+ metadata: {
102
+ totalCommits: {
103
+ type: Number,
104
+ default: 0,
105
+ },
106
+ totalPRs: {
107
+ type: Number,
108
+ default: 0,
109
+ },
110
+ totalIssues: {
111
+ type: Number,
112
+ default: 0,
113
+ },
114
+ categories: [
115
+ {
116
+ type: String,
117
+ enum: [
118
+ "feat",
119
+ "feature",
120
+ "enhancement",
121
+ "fix",
122
+ "bugfix",
123
+ "bug",
124
+ "docs",
125
+ "documentation",
126
+ "style",
127
+ "formatting",
128
+ "refactor",
129
+ "refactoring",
130
+ "test",
131
+ "testing",
132
+ "chore",
133
+ "maintenance",
134
+ "perf",
135
+ "performance",
136
+ "ci",
137
+ "continuous-integration",
138
+ "deps",
139
+ "dependencies",
140
+ ],
141
+ },
142
+ ],
143
+ authors: [
144
+ {
145
+ name: String,
146
+ email: String,
147
+ commits: Number,
148
+ },
149
+ ],
150
+ dateRange: {
151
+ start: Date,
152
+ end: Date,
153
+ },
154
+ },
155
+
156
+ // GitHub Integration
157
+ github: {
158
+ releaseUrl: {
159
+ type: String,
160
+ trim: true,
161
+ },
162
+ releaseTag: {
163
+ type: String,
164
+ trim: true,
165
+ },
166
+ releaseNotes: {
167
+ type: String,
168
+ },
169
+ autoCreatedRelease: {
170
+ type: Boolean,
171
+ default: false,
172
+ },
173
+ },
174
+
175
+ // Sharing and Privacy
176
+ isPublic: {
177
+ type: Boolean,
178
+ default: false,
179
+ },
180
+ shareToken: {
181
+ type: String,
182
+ unique: true,
183
+ sparse: true,
184
+ },
185
+ viewCount: {
186
+ type: Number,
187
+ default: 0,
188
+ },
189
+ lastViewedAt: {
190
+ type: Date,
191
+ },
192
+
193
+ // Template and Customization
194
+ template: {
195
+ type: String,
196
+ enum: ["default", "angular", "conventional", "custom"],
197
+ default: "default",
198
+ },
199
+ customTemplate: {
200
+ type: String,
201
+ },
202
+
203
+ // Version Information
204
+ version: {
205
+ major: {
206
+ type: Number,
207
+ default: 0,
208
+ },
209
+ minor: {
210
+ type: Number,
211
+ default: 0,
212
+ },
213
+ patch: {
214
+ type: Number,
215
+ default: 0,
216
+ },
217
+ preRelease: {
218
+ type: String,
219
+ },
220
+ build: {
221
+ type: String,
222
+ },
223
+ },
224
+ },
225
+ {
226
+ timestamps: true,
227
+ },
228
+ );
229
+
230
+ // Compound indexes
231
+ changelogSchema.index({ projectId: 1, month: 1, year: 1 }, { unique: true });
232
+ changelogSchema.index({ repositoryId: 1, month: 1, year: 1 }, { unique: true });
233
+ changelogSchema.index({ status: 1, generatedAt: 1 });
234
+ changelogSchema.index({ generatedBy: 1, createdAt: -1 });
235
+
236
+ // Pre-save middleware to generate share token
237
+ changelogSchema.pre("save", function (next) {
238
+ if (this.isNew && this.isPublic && !this.shareToken) {
239
+ const crypto = require("crypto");
240
+ this.shareToken = crypto.randomBytes(16).toString("hex");
241
+ }
242
+ next();
243
+ });
244
+
245
+ // Static method to find changelogs by project
246
+ changelogSchema.statics.findByProject = function (projectId, options = {}) {
247
+ const {
248
+ status = ["completed"],
249
+ limit = 20,
250
+ skip = 0,
251
+ sortBy = "createdAt",
252
+ sortOrder = -1,
253
+ } = options;
254
+
255
+ return this.find({
256
+ projectId,
257
+ status: { $in: Array.isArray(status) ? status : [status] },
258
+ })
259
+ .sort({ [sortBy]: sortOrder })
260
+ .limit(limit)
261
+ .skip(skip)
262
+ .populate("repositoryId", "name githubRepoName")
263
+ .populate("generatedBy", "name email");
264
+ };
265
+
266
+ // Static method to find changelogs by repository
267
+ changelogSchema.statics.findByRepository = function (
268
+ repositoryId,
269
+ options = {},
270
+ ) {
271
+ const {
272
+ status = ["completed"],
273
+ limit = 20,
274
+ skip = 0,
275
+ sortBy = "createdAt",
276
+ sortOrder = -1,
277
+ } = options;
278
+
279
+ return this.find({
280
+ repositoryId,
281
+ status: { $in: Array.isArray(status) ? status : [status] },
282
+ })
283
+ .sort({ [sortBy]: sortOrder })
284
+ .limit(limit)
285
+ .skip(skip)
286
+ .populate("projectId", "name slug")
287
+ .populate("generatedBy", "name email");
288
+ };
289
+
290
+ // Static method to get available months for a repository
291
+ changelogSchema.statics.getAvailableMonths = async function (repositoryId) {
292
+ const repository = await mongoose.model("Repository").findById(repositoryId);
293
+ if (!repository) {
294
+ throw new Error("Repository not found");
295
+ }
296
+
297
+ // Get commit history from repository
298
+ const commits = await repository.fetchCommitHistory();
299
+
300
+ // Get already generated changelogs
301
+ const generatedChangelogs = await this.find({
302
+ repositoryId,
303
+ status: "completed",
304
+ })
305
+ .select("month year")
306
+ .lean();
307
+
308
+ const generatedMonths = new Set(
309
+ generatedChangelogs.map((c) => `${c.year}-${c.month}`),
310
+ );
311
+
312
+ // Extract unique months from commits
313
+ const availableMonths = new Set();
314
+
315
+ commits.forEach((commit) => {
316
+ const date = new Date(commit.date);
317
+ const year = date.getFullYear();
318
+ const month = String(date.getMonth() + 1).padStart(2, "0");
319
+ const key = `${year}-${month}`;
320
+ availableMonths.add(key);
321
+ });
322
+
323
+ // Return months that have commits but no generated changelog
324
+ const result = Array.from(availableMonths)
325
+ .filter((month) => !generatedMonths.has(month))
326
+ .sort()
327
+ .reverse();
328
+
329
+ return result;
330
+ };
331
+
332
+ // Static method to check if changelog exists for a specific month/year
333
+ changelogSchema.statics.existsForPeriod = function (repositoryId, month, year) {
334
+ return this.exists({
335
+ repositoryId,
336
+ month,
337
+ year,
338
+ status: { $ne: "deleted" },
339
+ });
340
+ };
341
+
342
+ // Instance method to update generation status
343
+ changelogSchema.methods.updateStatus = async function (status, error = null) {
344
+ const updates = {
345
+ status,
346
+ generatedAt: status === "completed" ? new Date() : this.generatedAt,
347
+ };
348
+
349
+ if (error) {
350
+ updates.error = error;
351
+ updates.status = "failed";
352
+ } else {
353
+ updates.error = null;
354
+ }
355
+
356
+ return this.updateOne(updates);
357
+ };
358
+
359
+ // Instance method to update content and metadata
360
+ changelogSchema.methods.updateContent = async function (
361
+ content,
362
+ metadata = {},
363
+ ) {
364
+ const updates = {
365
+ content,
366
+ "metadata.totalCommits":
367
+ metadata.totalCommits || this.metadata.totalCommits,
368
+ "metadata.totalPRs": metadata.totalPRs || this.metadata.totalPRs,
369
+ "metadata.totalIssues": metadata.totalIssues || this.metadata.totalIssues,
370
+ "metadata.categories": metadata.categories || this.metadata.categories,
371
+ "metadata.authors": metadata.authors || this.metadata.authors,
372
+ "metadata.dateRange": metadata.dateRange || this.metadata.dateRange,
373
+ };
374
+
375
+ return this.updateOne(updates);
376
+ };
377
+
378
+ // Instance method to increment view count
379
+ changelogSchema.methods.incrementViewCount = async function () {
380
+ return this.updateOne({
381
+ $inc: { viewCount: 1 },
382
+ $set: { lastViewedAt: new Date() },
383
+ });
384
+ };
385
+
386
+ // Instance method to get changelog statistics
387
+ changelogSchema.methods.getStatistics = async function () {
388
+ const repository = await mongoose
389
+ .model("Repository")
390
+ .findById(this.repositoryId);
391
+ const project = await mongoose.model("Project").findById(this.projectId);
392
+
393
+ return {
394
+ project: {
395
+ name: project?.name,
396
+ slug: project?.slug,
397
+ },
398
+ repository: {
399
+ name: repository?.name,
400
+ githubRepoName: repository?.githubRepoName,
401
+ },
402
+ changelog: {
403
+ title: this.title,
404
+ month: this.month,
405
+ year: this.year,
406
+ status: this.status,
407
+ format: this.format,
408
+ viewCount: this.viewCount,
409
+ generatedAt: this.generatedAt,
410
+ generationTime: this.generationTime,
411
+ },
412
+ content: {
413
+ totalCommits: this.metadata.totalCommits,
414
+ totalPRs: this.metadata.totalPRs,
415
+ totalIssues: this.metadata.totalIssues,
416
+ categories: this.metadata.categories,
417
+ authors: this.metadata.authors,
418
+ dateRange: this.metadata.dateRange,
419
+ },
420
+ };
421
+ };
422
+
423
+ // Instance method to generate version number
424
+ changelogSchema.methods.generateVersion = function () {
425
+ const major = this.metadata.categories.includes("feat") ? 1 : 0;
426
+ const minor = this.metadata.categories.includes("fix") ? 1 : 0;
427
+ const patch = this.metadata.categories.includes("docs") ? 1 : 0;
428
+
429
+ return {
430
+ major: this.version.major + major,
431
+ minor: this.version.minor + minor,
432
+ patch: this.version.patch + patch,
433
+ };
434
+ };
435
+
436
+ // Clean up response
437
+ changelogSchema.methods.toJSON = function () {
438
+ const obj = this.toObject();
439
+ delete obj.__v;
440
+ return obj;
441
+ };
442
+
443
+ module.exports = mongoose.model("Changelog", changelogSchema);
@@ -0,0 +1,226 @@
1
+ const mongoose = require("mongoose");
2
+
3
+ const projectSchema = new mongoose.Schema(
4
+ {
5
+ name: {
6
+ type: String,
7
+ required: [true, "Project name is required"],
8
+ trim: true,
9
+ maxlength: [100, "Project name cannot exceed 100 characters"],
10
+ },
11
+ description: {
12
+ type: String,
13
+ trim: true,
14
+ maxlength: [500, "Description cannot exceed 500 characters"],
15
+ },
16
+ slug: {
17
+ type: String,
18
+ unique: true,
19
+ lowercase: true,
20
+ trim: true,
21
+ },
22
+
23
+ // Ownership - uses SuperBackend's User and Organization models
24
+ ownerId: {
25
+ type: mongoose.Schema.Types.ObjectId,
26
+ ref: "users", // SuperBackend User model
27
+ required: true,
28
+ },
29
+ organizationId: {
30
+ type: mongoose.Schema.Types.ObjectId,
31
+ ref: "organizations", // SuperBackend Organization model
32
+ },
33
+
34
+ // Status
35
+ status: {
36
+ type: String,
37
+ enum: ["active", "archived", "deleted"],
38
+ default: "active",
39
+ },
40
+
41
+ // Settings
42
+ settings: {
43
+ defaultBranch: {
44
+ type: String,
45
+ default: "main",
46
+ },
47
+ changelogTemplate: {
48
+ type: String,
49
+ enum: ["default", "angular", "conventional"],
50
+ default: "default",
51
+ },
52
+ includeCommitsWithoutPR: {
53
+ type: Boolean,
54
+ default: true,
55
+ },
56
+ includeMergeCommits: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
60
+ },
61
+
62
+ // Metadata
63
+ metadata: {
64
+ createdAt: {
65
+ type: Date,
66
+ default: Date.now,
67
+ },
68
+ lastChangelogGenerated: {
69
+ type: Date,
70
+ },
71
+ totalRepositories: {
72
+ type: Number,
73
+ default: 0,
74
+ },
75
+ totalChangelogs: {
76
+ type: Number,
77
+ default: 0,
78
+ },
79
+ },
80
+
81
+ // Tags/Categories
82
+ tags: [
83
+ {
84
+ type: String,
85
+ trim: true,
86
+ lowercase: true,
87
+ },
88
+ ],
89
+
90
+ // Privacy
91
+ isPublic: {
92
+ type: Boolean,
93
+ default: false,
94
+ },
95
+
96
+ // GitHub Integration
97
+ githubIntegration: {
98
+ enabled: {
99
+ type: Boolean,
100
+ default: false,
101
+ },
102
+ webhookSecret: {
103
+ type: String,
104
+ },
105
+ autoGenerateOnPush: {
106
+ type: Boolean,
107
+ default: false,
108
+ },
109
+ },
110
+ },
111
+ {
112
+ timestamps: true,
113
+ },
114
+ );
115
+
116
+ // Indexes
117
+ projectSchema.index({ ownerId: 1, status: 1 });
118
+ projectSchema.index({ organizationId: 1, status: 1 });
119
+ projectSchema.index({ tags: 1 });
120
+
121
+ // Pre-save middleware to generate slug
122
+ projectSchema.pre("save", async function (next) {
123
+ if (this.isModified("name")) {
124
+ let baseSlug = this.name
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9]+/g, "-")
127
+ .replace(/^-|-$/g, "");
128
+
129
+ // Check if slug already exists
130
+ const existingProject = await this.constructor.findOne({
131
+ slug: new RegExp(`^${baseSlug}(-\d+)?$`, "i"),
132
+ });
133
+ if (
134
+ existingProject &&
135
+ existingProject._id.toString() !== this._id.toString()
136
+ ) {
137
+ // Add timestamp or incrementing number to make unique
138
+ this.slug = `${baseSlug}-${Date.now().toString(36)}`;
139
+ } else {
140
+ this.slug = baseSlug;
141
+ }
142
+ }
143
+ next();
144
+ });
145
+
146
+ // Static method to find projects by user
147
+ projectSchema.statics.findByUser = function (userId, options = {}) {
148
+ const { status = "active", limit = 10, skip = 0 } = options;
149
+
150
+ return this.find({
151
+ ownerId: userId,
152
+ status,
153
+ })
154
+ .sort({ updatedAt: -1 })
155
+ .limit(limit)
156
+ .skip(skip)
157
+ .populate("ownerId", "name email avatar");
158
+ };
159
+
160
+ // Static method to check if user can create more projects
161
+ projectSchema.statics.canCreateProject = async function (userId) {
162
+ const user = await mongoose.model("users").findById(userId);
163
+ if (!user) return false;
164
+
165
+ const currentProjects = await this.countDocuments({
166
+ ownerId: userId,
167
+ status: "active",
168
+ });
169
+
170
+ return currentProjects < user.subscription.projectsLimit;
171
+ };
172
+
173
+ // Instance method to update repository count
174
+ projectSchema.methods.updateRepositoryCount = async function () {
175
+ const Repository = mongoose.model("Repository");
176
+ const count = await Repository.countDocuments({
177
+ projectId: this._id,
178
+ status: "active",
179
+ });
180
+
181
+ return this.updateOne({ "metadata.totalRepositories": count });
182
+ };
183
+
184
+ // Instance method to update changelog count
185
+ projectSchema.methods.updateChangelogCount = async function () {
186
+ const Changelog = mongoose.model("Changelog");
187
+ const count = await Changelog.countDocuments({
188
+ projectId: this._id,
189
+ status: { $in: ["completed", "failed"] },
190
+ });
191
+
192
+ return this.updateOne({ "metadata.totalChangelogs": count });
193
+ };
194
+
195
+ // Instance method to get project statistics
196
+ projectSchema.methods.getStatistics = async function () {
197
+ const Repository = mongoose.model("Repository");
198
+ const Changelog = mongoose.model("Changelog");
199
+
200
+ const [repositories, changelogs] = await Promise.all([
201
+ Repository.find({ projectId: this._id, status: "active" })
202
+ .select("name githubRepoName lastSyncedAt")
203
+ .lean(),
204
+ Changelog.find({ projectId: this._id })
205
+ .sort({ createdAt: -1 })
206
+ .limit(5)
207
+ .select("month year status createdAt")
208
+ .lean(),
209
+ ]);
210
+
211
+ return {
212
+ repositories,
213
+ recentChangelogs: changelogs,
214
+ totalRepositories: repositories.length,
215
+ totalChangelogs: await Changelog.countDocuments({ projectId: this._id }),
216
+ };
217
+ };
218
+
219
+ // Clean up response
220
+ projectSchema.methods.toJSON = function () {
221
+ const obj = this.toObject();
222
+ delete obj.__v;
223
+ return obj;
224
+ };
225
+
226
+ module.exports = mongoose.model("Project", projectSchema);