@intranefr/superbackend 1.5.0 → 1.5.2

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 (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -55,14 +55,35 @@ async function tryAttachUser(req) {
55
55
 
56
56
  exports.track = async (req, res) => {
57
57
  try {
58
+ // Validate request size to prevent abuse
59
+ const contentLength = req.headers['content-length'];
60
+ if (contentLength && parseInt(contentLength) > 1024 * 10) { // 10KB limit
61
+ return res.status(413).json({ error: 'Request too large' });
62
+ }
63
+
58
64
  await tryAttachUser(req);
59
65
 
60
66
  const action = String(req.body?.action || '').trim();
61
67
  const meta = req.body?.meta ?? null;
62
68
 
69
+ // Validate action field
63
70
  if (!action) {
64
71
  return res.status(400).json({ error: 'action is required' });
65
72
  }
73
+
74
+ if (action.length > 100) {
75
+ return res.status(400).json({ error: 'action too long (max 100 characters)' });
76
+ }
77
+
78
+ // Validate action format (allow only alphanumeric, underscores, hyphens, dots)
79
+ if (!/^[a-zA-Z0-9._-]+$/.test(action)) {
80
+ return res.status(400).json({ error: 'invalid action format' });
81
+ }
82
+
83
+ // Validate meta size
84
+ if (meta && JSON.stringify(meta).length > 1024 * 5) { // 5KB limit
85
+ return res.status(400).json({ error: 'meta data too large' });
86
+ }
66
87
 
67
88
  let actorType = 'anonymous';
68
89
  let actorId = getAnonId(req);
@@ -98,12 +119,48 @@ exports.track = async (req, res) => {
98
119
 
99
120
  exports.getImpact = async (req, res) => {
100
121
  try {
101
- const { start, end } = getMonthRange(new Date());
122
+ // Validate query parameters
123
+ const { start, end } = req.query;
124
+
125
+ // Allow custom time ranges but restrict to reasonable limits
126
+ let startTime, endTime;
127
+
128
+ if (start || end) {
129
+ // Parse custom range if provided
130
+ startTime = start ? new Date(start) : null;
131
+ endTime = end ? new Date(end) : null;
132
+
133
+ // Validate dates
134
+ if ((start && isNaN(startTime.getTime())) || (end && isNaN(endTime.getTime()))) {
135
+ return res.status(400).json({ error: 'Invalid date format' });
136
+ }
137
+
138
+ // Restrict range to maximum 1 year
139
+ if (startTime && endTime) {
140
+ const rangeMs = endTime.getTime() - startTime.getTime();
141
+ const maxRangeMs = 365 * 24 * 60 * 60 * 1000; // 1 year
142
+ if (rangeMs > maxRangeMs) {
143
+ return res.status(400).json({ error: 'Time range too large (max 1 year)' });
144
+ }
145
+ }
146
+
147
+ // Default to current month if range is incomplete
148
+ if (!startTime || !endTime) {
149
+ const currentMonth = getMonthRange(new Date());
150
+ startTime = startTime || currentMonth.start;
151
+ endTime = endTime || currentMonth.end;
152
+ }
153
+ } else {
154
+ // Default to current month
155
+ const currentMonth = getMonthRange(new Date());
156
+ startTime = currentMonth.start;
157
+ endTime = currentMonth.end;
158
+ }
102
159
 
103
160
  const activeActorsAgg = await ActionEvent.aggregate([
104
161
  {
105
162
  $match: {
106
- createdAt: { $gte: start, $lt: end },
163
+ createdAt: { $gte: startTime, $lt: endTime },
107
164
  actorType: { $in: ['user', 'anonymous'] },
108
165
  },
109
166
  },
@@ -122,7 +179,7 @@ exports.getImpact = async (req, res) => {
122
179
 
123
180
  const servicesConsulted = await ActionEvent.countDocuments({
124
181
  action: 'service_view',
125
- createdAt: { $gte: start, $lt: end },
182
+ createdAt: { $gte: startTime, $lt: endTime },
126
183
  });
127
184
 
128
185
  const newsletterSetting = await GlobalSetting.findOne({ key: 'newsletter_list' }).lean();
@@ -136,8 +193,11 @@ exports.getImpact = async (req, res) => {
136
193
  }
137
194
  }
138
195
 
196
+ // Add cache headers for better performance
197
+ res.set('Cache-Control', 'public, max-age=300'); // 5 minutes cache
198
+
139
199
  return res.json({
140
- range: { start: start.toISOString(), end: end.toISOString() },
200
+ range: { start: startTime.toISOString(), end: endTime.toISOString() },
141
201
  activeUsers,
142
202
  servicesConsulted,
143
203
  newsletterSubscribers,
@@ -270,6 +270,86 @@ exports.removeMember = async (req, res) => {
270
270
  }
271
271
  };
272
272
 
273
+ exports.addMember = async (req, res) => {
274
+ try {
275
+ const { orgId } = req.params;
276
+ const { userId, role } = req.body;
277
+
278
+ if (!orgId || !mongoose.Types.ObjectId.isValid(String(orgId))) {
279
+ return res.status(400).json({ error: 'Invalid organization ID' });
280
+ }
281
+
282
+ if (!userId || !mongoose.Types.ObjectId.isValid(String(userId))) {
283
+ return res.status(400).json({ error: 'Invalid user ID' });
284
+ }
285
+
286
+ // Validate role
287
+ const defaultRole = await getDefaultOrgRole();
288
+ const memberRole = role || defaultRole;
289
+ if (!(await isValidOrgRole(memberRole))) {
290
+ const allowed = await getAllowedOrgRoles();
291
+ return res.status(400).json({ error: 'Invalid role', allowedRoles: allowed });
292
+ }
293
+
294
+ // Check if organization exists
295
+ const org = await Organization.findById(orgId);
296
+ if (!org) {
297
+ return res.status(404).json({ error: 'Organization not found' });
298
+ }
299
+
300
+ // Check if user exists
301
+ const user = await User.findById(userId);
302
+ if (!user) {
303
+ return res.status(404).json({ error: 'User not found' });
304
+ }
305
+
306
+ // Check if user is already a member
307
+ const existingMember = await OrganizationMember.findOne({
308
+ orgId,
309
+ userId,
310
+ status: { $in: ['active', 'removed'] }
311
+ });
312
+
313
+ if (existingMember) {
314
+ if (existingMember.status === 'active') {
315
+ return res.status(409).json({ error: 'User is already a member of this organization' });
316
+ } else {
317
+ // Reactivate removed member
318
+ existingMember.status = 'active';
319
+ existingMember.role = memberRole;
320
+ existingMember.addedByUserId = req.user?.id || org.ownerUserId;
321
+ await existingMember.save();
322
+ return res.json({
323
+ message: 'Member reactivated successfully',
324
+ member: existingMember.toObject()
325
+ });
326
+ }
327
+ }
328
+
329
+ // Create new member
330
+ const member = await OrganizationMember.create({
331
+ orgId,
332
+ userId,
333
+ role: memberRole,
334
+ status: 'active',
335
+ addedByUserId: req.user?.id || org.ownerUserId,
336
+ });
337
+
338
+ // Populate user data for response
339
+ const populatedMember = await OrganizationMember.findById(member._id)
340
+ .populate('userId', 'email name')
341
+ .lean();
342
+
343
+ return res.status(201).json({
344
+ message: 'Member added successfully',
345
+ member: populatedMember
346
+ });
347
+ } catch (error) {
348
+ console.error('Admin org member add error:', error);
349
+ return res.status(500).json({ error: 'Failed to add member' });
350
+ }
351
+ };
352
+
273
353
  exports.listInvites = async (req, res) => {
274
354
  try {
275
355
  const { orgId } = req.params;
@@ -0,0 +1,258 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Mongoose connection state management
5
+ * Provides centralized connection handling with reference counting and automatic cleanup
6
+ */
7
+ class MongooseHelper {
8
+ constructor() {
9
+ this.connectionPromise = null;
10
+ this.isConnected = false;
11
+ this.connectionCount = 0;
12
+ this.connectionOptions = null;
13
+ }
14
+
15
+ /**
16
+ * Get MongoDB URI from environment with fallbacks
17
+ * @returns {string} MongoDB connection URI
18
+ */
19
+ getMongoUri() {
20
+ const uri = process.env.MONGODB_URI || process.env.MONGO_URI || 'mongodb://localhost:27017/myappdb';
21
+ if (!uri) {
22
+ throw new Error('Missing MONGODB_URI or MONGO_URI environment variable');
23
+ }
24
+ return uri;
25
+ }
26
+
27
+ /**
28
+ * Get mongoose connection options
29
+ * @returns {Object} Connection options
30
+ */
31
+ getConnectionOptions() {
32
+ if (this.connectionOptions) {
33
+ return this.connectionOptions;
34
+ }
35
+
36
+ this.connectionOptions = {
37
+ serverSelectionTimeoutMS: 5000,
38
+ maxPoolSize: 2, // Conservative for scripts
39
+ bufferCommands: false,
40
+ // Add retry settings for reliability
41
+ retryWrites: true,
42
+ retryReads: true,
43
+ // Add socket settings for scripts
44
+ socketTimeoutMS: 30000,
45
+ connectTimeoutMS: 10000,
46
+ };
47
+
48
+ return this.connectionOptions;
49
+ }
50
+
51
+ /**
52
+ * Connect to MongoDB (singleton pattern)
53
+ * @returns {Promise<mongoose.Connection>} Mongoose connection
54
+ */
55
+ async connect() {
56
+ // Return existing connection if already connected
57
+ if (this.isConnected && mongoose.connection.readyState === 1) {
58
+ this.connectionCount++;
59
+ return mongoose.connection;
60
+ }
61
+
62
+ // Return existing promise if connection is in progress
63
+ if (this.connectionPromise) {
64
+ await this.connectionPromise;
65
+ this.connectionCount++;
66
+ return mongoose.connection;
67
+ }
68
+
69
+ // Create new connection promise
70
+ this.connectionPromise = this._createConnection();
71
+ await this.connectionPromise;
72
+ this.connectionCount++;
73
+ return mongoose.connection;
74
+ }
75
+
76
+ /**
77
+ * Internal connection creation
78
+ * @private
79
+ * @returns {Promise<mongoose.Connection>}
80
+ */
81
+ async _createConnection() {
82
+ try {
83
+ const uri = this.getMongoUri();
84
+ const options = this.getConnectionOptions();
85
+
86
+ console.log(`[MongooseHelper] Connecting to MongoDB...`);
87
+
88
+ // Clear any existing connection
89
+ if (mongoose.connection.readyState !== 0) {
90
+ await mongoose.disconnect();
91
+ }
92
+
93
+ await mongoose.connect(uri, options);
94
+
95
+ this.isConnected = true;
96
+
97
+ console.log(`[MongooseHelper] ✅ Connected to MongoDB`);
98
+
99
+ // Setup connection error handling
100
+ mongoose.connection.on('error', (error) => {
101
+ console.error('[MongooseHelper] Connection error:', error);
102
+ this.isConnected = false;
103
+ this.connectionPromise = null;
104
+ });
105
+
106
+ mongoose.connection.on('disconnected', () => {
107
+ console.log('[MongooseHelper] Disconnected from MongoDB');
108
+ this.isConnected = false;
109
+ this.connectionPromise = null;
110
+ });
111
+
112
+ mongoose.connection.on('reconnected', () => {
113
+ console.log('[MongooseHelper] Reconnected to MongoDB');
114
+ this.isConnected = true;
115
+ });
116
+
117
+ return mongoose.connection;
118
+ } catch (error) {
119
+ this.connectionPromise = null;
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Disconnect from MongoDB (reference counting)
126
+ * @returns {Promise<void>}
127
+ */
128
+ async disconnect() {
129
+ if (!this.isConnected) {
130
+ return;
131
+ }
132
+
133
+ this.connectionCount--;
134
+
135
+ // Only disconnect if no more references
136
+ if (this.connectionCount <= 0) {
137
+ try {
138
+ await mongoose.disconnect();
139
+ console.log('[MongooseHelper] ✅ Disconnected from MongoDB');
140
+ } catch (error) {
141
+ console.error('[MongooseHelper] Disconnect error:', error);
142
+ } finally {
143
+ this.isConnected = false;
144
+ this.connectionPromise = null;
145
+ this.connectionCount = 0;
146
+ }
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Force disconnect regardless of reference count
152
+ * @returns {Promise<void>}
153
+ */
154
+ async forceDisconnect() {
155
+ try {
156
+ if (mongoose.connection.readyState !== 0) {
157
+ await mongoose.disconnect();
158
+ console.log('[MongooseHelper] ✅ Force disconnected from MongoDB');
159
+ }
160
+ } catch (error) {
161
+ console.error('[MongooseHelper] Force disconnect error:', error);
162
+ } finally {
163
+ this.isConnected = false;
164
+ this.connectionPromise = null;
165
+ this.connectionCount = 0;
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get connection status
171
+ * @returns {Object} Connection status info
172
+ */
173
+ getStatus() {
174
+ const readyStateMap = {
175
+ 0: 'disconnected',
176
+ 1: 'connected',
177
+ 2: 'connecting',
178
+ 3: 'disconnecting'
179
+ };
180
+
181
+ return {
182
+ isConnected: this.isConnected,
183
+ readyState: mongoose.connection.readyState,
184
+ readyStateText: readyStateMap[mongoose.connection.readyState] || 'unknown',
185
+ connectionCount: this.connectionCount,
186
+ host: mongoose.connection.host,
187
+ port: mongoose.connection.port,
188
+ name: mongoose.connection.name,
189
+ hasActiveConnection: mongoose.connection.readyState === 1
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Execute function with automatic connection management
195
+ * @param {Function} fn - Async function to execute
196
+ * @param {Object} options - Options
197
+ * @returns {Promise<any>} Function result
198
+ */
199
+ async withConnection(fn, options = {}) {
200
+ await this.connect();
201
+
202
+ try {
203
+ const result = await fn(mongoose);
204
+ return result;
205
+ } finally {
206
+ if (options.autoDisconnect !== false) {
207
+ await this.disconnect();
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Wait for connection to be ready
214
+ * @param {number} timeout - Timeout in milliseconds
215
+ * @returns {Promise<void>}
216
+ */
217
+ async waitForConnection(timeout = 10000) {
218
+ const startTime = Date.now();
219
+
220
+ while (mongoose.connection.readyState !== 1) {
221
+ if (Date.now() - startTime > timeout) {
222
+ throw new Error(`Connection timeout after ${timeout}ms`);
223
+ }
224
+
225
+ if (mongoose.connection.readyState === 0) {
226
+ throw new Error('Connection is disconnected');
227
+ }
228
+
229
+ await new Promise(resolve => setTimeout(resolve, 100));
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Reset the helper state (useful for testing)
235
+ */
236
+ reset() {
237
+ this.connectionPromise = null;
238
+ this.isConnected = false;
239
+ this.connectionCount = 0;
240
+ this.connectionOptions = null;
241
+ }
242
+ }
243
+
244
+ // Singleton instance
245
+ const mongooseHelper = new MongooseHelper();
246
+
247
+ module.exports = {
248
+ MongooseHelper,
249
+ mongooseHelper,
250
+ connect: () => mongooseHelper.connect(),
251
+ disconnect: () => mongooseHelper.disconnect(),
252
+ forceDisconnect: () => mongooseHelper.forceDisconnect(),
253
+ withConnection: (fn, options) => mongooseHelper.withConnection(fn, options),
254
+ getStatus: () => mongooseHelper.getStatus(),
255
+ getMongoUri: () => mongooseHelper.getMongoUri(),
256
+ waitForConnection: (timeout) => mongooseHelper.waitForConnection(timeout),
257
+ reset: () => mongooseHelper.reset()
258
+ };
@@ -0,0 +1,230 @@
1
+ const { mongooseHelper } = require('./mongooseHelper');
2
+
3
+ /**
4
+ * Base class for scripts with database connectivity
5
+ * Provides automatic connection management and error handling
6
+ */
7
+ class ScriptBase {
8
+ constructor(options = {}) {
9
+ this.name = options.name || this.constructor.name;
10
+ this.autoDisconnect = options.autoDisconnect !== false;
11
+ this.timeout = options.timeout || 300000; // 5 minutes default
12
+ this.startTime = null;
13
+ this.context = null;
14
+ }
15
+
16
+ /**
17
+ * Main script execution method (to be implemented by subclasses)
18
+ * @param {Object} context - Execution context with mongoose instance
19
+ * @returns {Promise<any>} Script result
20
+ */
21
+ async execute(context) {
22
+ throw new Error('execute method must be implemented by subclass');
23
+ }
24
+
25
+ /**
26
+ * Setup method called before execution (optional override)
27
+ * @param {Object} context - Execution context
28
+ * @returns {Promise<void>}
29
+ */
30
+ async setup(context) {
31
+ // Override in subclasses if needed
32
+ }
33
+
34
+ /**
35
+ * Cleanup method called after execution (optional override)
36
+ * @param {Object} context - Execution context
37
+ * @returns {Promise<void>}
38
+ */
39
+ async cleanup(context) {
40
+ // Override in subclasses if needed
41
+ }
42
+
43
+ /**
44
+ * Run the script with automatic database connection management
45
+ * @returns {Promise<any>} Script result
46
+ */
47
+ async run() {
48
+ this.startTime = Date.now();
49
+
50
+ // Set up timeout handling
51
+ const timeoutPromise = new Promise((_, reject) => {
52
+ setTimeout(() => {
53
+ reject(new Error(`Script ${this.name} timed out after ${this.timeout}ms`));
54
+ }, this.timeout);
55
+ });
56
+
57
+ try {
58
+ console.log(`[${this.name}] Starting script execution...`);
59
+
60
+ const executionPromise = this._executeWithConnection();
61
+ const result = await Promise.race([executionPromise, timeoutPromise]);
62
+
63
+ const duration = Date.now() - this.startTime;
64
+ console.log(`[${this.name}] ✅ Completed in ${duration}ms`);
65
+
66
+ return result;
67
+ } catch (error) {
68
+ const duration = Date.now() - this.startTime;
69
+ console.error(`[${this.name}] ❌ Failed after ${duration}ms:`, error.message);
70
+
71
+ // Ensure cleanup on error
72
+ await this._handleError(error);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Internal execution method with connection management
79
+ * @private
80
+ * @returns {Promise<any>}
81
+ */
82
+ async _executeWithConnection() {
83
+ return await mongooseHelper.withConnection(
84
+ async (mongoose) => {
85
+ // Create execution context
86
+ this.context = {
87
+ mongoose,
88
+ models: mongoose.models,
89
+ connection: mongoose.connection,
90
+ db: mongoose.connection.db,
91
+ script: {
92
+ name: this.name,
93
+ startTime: this.startTime,
94
+ timeout: this.timeout
95
+ }
96
+ };
97
+
98
+ // Call setup
99
+ await this.setup(this.context);
100
+
101
+ try {
102
+ // Execute main logic
103
+ const result = await this.execute(this.context);
104
+ return result;
105
+ } finally {
106
+ // Call cleanup
107
+ await this.cleanup(this.context);
108
+ }
109
+ },
110
+ { autoDisconnect: this.autoDisconnect }
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Handle script errors and cleanup
116
+ * @private
117
+ * @param {Error} error - The error that occurred
118
+ */
119
+ async _handleError(error) {
120
+ try {
121
+ // Call cleanup with error context if available
122
+ if (this.context) {
123
+ await this.cleanup(this.context);
124
+ }
125
+ } catch (cleanupError) {
126
+ console.error(`[${this.name}] Cleanup error:`, cleanupError.message);
127
+ }
128
+
129
+ // Force disconnect on error
130
+ await mongooseHelper.forceDisconnect();
131
+ }
132
+
133
+ /**
134
+ * Get script execution status
135
+ * @returns {Object} Status information
136
+ */
137
+ getStatus() {
138
+ return {
139
+ name: this.name,
140
+ isRunning: this.startTime !== null,
141
+ startTime: this.startTime,
142
+ duration: this.startTime ? Date.now() - this.startTime : null,
143
+ timeout: this.timeout,
144
+ autoDisconnect: this.autoDisconnect,
145
+ connectionStatus: mongooseHelper.getStatus()
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Validate script configuration
151
+ * @returns {Object} Validation result
152
+ */
153
+ validate() {
154
+ const errors = [];
155
+ const warnings = [];
156
+
157
+ // Check if execute method is implemented
158
+ if (this.execute === ScriptBase.prototype.execute) {
159
+ errors.push('execute method must be implemented');
160
+ }
161
+
162
+ // Check timeout value
163
+ if (this.timeout <= 0) {
164
+ errors.push('timeout must be greater than 0');
165
+ }
166
+
167
+ if (this.timeout > 3600000) { // 1 hour
168
+ warnings.push('timeout is very long (> 1 hour)');
169
+ }
170
+
171
+ // Check environment
172
+ if (!process.env.MONGODB_URI && !process.env.MONGO_URI) {
173
+ warnings.push('No MongoDB URI environment variable set, will use localhost');
174
+ }
175
+
176
+ return {
177
+ valid: errors.length === 0,
178
+ errors,
179
+ warnings
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Create a child script with inherited configuration
185
+ * @param {Function} ChildScriptClass - Child script class
186
+ * @param {Object} options - Additional options for child
187
+ * @returns {ScriptBase} Child script instance
188
+ */
189
+ createChild(ChildScriptClass, options = {}) {
190
+ const childOptions = {
191
+ timeout: this.timeout,
192
+ autoDisconnect: this.autoDisconnect,
193
+ ...options
194
+ };
195
+
196
+ return new ChildScriptClass(childOptions);
197
+ }
198
+
199
+ /**
200
+ * Log script message with consistent formatting
201
+ * @param {string} level - Log level (info, warn, error, debug)
202
+ * @param {string} message - Message to log
203
+ * @param {any} data - Additional data to log
204
+ */
205
+ log(level, message, data = null) {
206
+ const timestamp = new Date().toISOString();
207
+ const prefix = `[${timestamp}][${this.name}]`;
208
+
209
+ switch (level) {
210
+ case 'info':
211
+ console.log(`${prefix} ${message}`, data || '');
212
+ break;
213
+ case 'warn':
214
+ console.warn(`${prefix} ⚠️ ${message}`, data || '');
215
+ break;
216
+ case 'error':
217
+ console.error(`${prefix} ❌ ${message}`, data || '');
218
+ break;
219
+ case 'debug':
220
+ if (process.env.DEBUG || process.env.NODE_ENV === 'development') {
221
+ console.debug(`${prefix} 🔍 ${message}`, data || '');
222
+ }
223
+ break;
224
+ default:
225
+ console.log(`${prefix} ${message}`, data || '');
226
+ }
227
+ }
228
+ }
229
+
230
+ module.exports = { ScriptBase };