@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.
- package/.env.example +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- 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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 };
|