@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
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
const CronJob = require('../models/CronJob');
|
|
2
|
+
const CronExecution = require('../models/CronExecution');
|
|
3
|
+
const parser = require('cron-parser');
|
|
4
|
+
const { startRun } = require('../services/scriptsRunner.service');
|
|
5
|
+
const cronScheduler = require('../services/cronScheduler.service');
|
|
6
|
+
|
|
7
|
+
function toSafeJsonError(error) {
|
|
8
|
+
const msg = error?.message || 'Operation failed';
|
|
9
|
+
const code = error?.code;
|
|
10
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
11
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
12
|
+
if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
|
|
13
|
+
return { status: 500, body: { error: msg } };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeEnv(env) {
|
|
17
|
+
const items = Array.isArray(env) ? env : [];
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const it of items) {
|
|
20
|
+
if (!it || typeof it !== 'object') continue;
|
|
21
|
+
const key = String(it.key || '').trim();
|
|
22
|
+
if (!key) continue;
|
|
23
|
+
out.push({ key, value: String(it.value || '') });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeHeaders(headers) {
|
|
29
|
+
const items = Array.isArray(headers) ? headers : [];
|
|
30
|
+
const out = [];
|
|
31
|
+
for (const it of items) {
|
|
32
|
+
if (!it || typeof it !== 'object') continue;
|
|
33
|
+
const key = String(it.key || '').trim();
|
|
34
|
+
if (!key) continue;
|
|
35
|
+
out.push({ key, value: String(it.value || '') });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function calculateNextRun(cronExpression, timezone = 'UTC') {
|
|
41
|
+
try {
|
|
42
|
+
const interval = parser.parseExpression(cronExpression, {
|
|
43
|
+
tz: timezone,
|
|
44
|
+
});
|
|
45
|
+
return interval.next().toDate();
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`Invalid cron expression: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
exports.listCronJobs = async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
const items = await CronJob.find()
|
|
54
|
+
.populate('scriptId', 'name type runner')
|
|
55
|
+
.sort({ updatedAt: -1 })
|
|
56
|
+
.lean();
|
|
57
|
+
res.json({ items });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const safe = toSafeJsonError(err);
|
|
60
|
+
res.status(safe.status).json(safe.body);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
exports.getCronJob = async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const doc = await CronJob.findById(req.params.id)
|
|
67
|
+
.populate('scriptId', 'name type runner')
|
|
68
|
+
.lean();
|
|
69
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
70
|
+
res.json({ item: doc });
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const safe = toSafeJsonError(err);
|
|
73
|
+
res.status(safe.status).json(safe.body);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
exports.createCronJob = async (req, res) => {
|
|
78
|
+
try {
|
|
79
|
+
const payload = req.body || {};
|
|
80
|
+
|
|
81
|
+
// Validate cron expression
|
|
82
|
+
const nextRunAt = calculateNextRun(payload.cronExpression, payload.timezone);
|
|
83
|
+
|
|
84
|
+
// Validate task type requirements
|
|
85
|
+
if (payload.taskType === 'script' && (!payload.scriptId || payload.scriptId === "")) {
|
|
86
|
+
return res.status(400).json({ error: 'Script ID is required for script-type cron jobs' });
|
|
87
|
+
}
|
|
88
|
+
if (payload.taskType === 'http' && (!payload.httpUrl || payload.httpUrl.trim() === "")) {
|
|
89
|
+
return res.status(400).json({ error: 'URL is required for HTTP-type cron jobs' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle scriptId - convert empty string to null for script jobs
|
|
93
|
+
let scriptId = payload.scriptId;
|
|
94
|
+
if (scriptId === "" || scriptId === null || scriptId === undefined) {
|
|
95
|
+
scriptId = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const doc = await CronJob.create({
|
|
99
|
+
name: String(payload.name || '').trim(),
|
|
100
|
+
description: String(payload.description || ''),
|
|
101
|
+
cronExpression: String(payload.cronExpression || '').trim(),
|
|
102
|
+
timezone: String(payload.timezone || 'UTC'),
|
|
103
|
+
enabled: payload.enabled === undefined ? true : Boolean(payload.enabled),
|
|
104
|
+
nextRunAt,
|
|
105
|
+
taskType: String(payload.taskType || '').trim(),
|
|
106
|
+
scriptId,
|
|
107
|
+
scriptEnv: normalizeEnv(payload.scriptEnv),
|
|
108
|
+
httpMethod: String(payload.httpMethod || 'GET'),
|
|
109
|
+
httpUrl: payload.taskType === 'http' ? String(payload.httpUrl || '').trim() : undefined,
|
|
110
|
+
httpHeaders: normalizeHeaders(payload.httpHeaders),
|
|
111
|
+
httpBody: String(payload.httpBody || ''),
|
|
112
|
+
httpBodyType: String(payload.httpBodyType || 'raw'),
|
|
113
|
+
httpAuth: {
|
|
114
|
+
type: String(payload.httpAuth?.type || 'none'),
|
|
115
|
+
token: String(payload.httpAuth?.token || ''),
|
|
116
|
+
username: String(payload.httpAuth?.username || ''),
|
|
117
|
+
password: String(payload.httpAuth?.password || ''),
|
|
118
|
+
},
|
|
119
|
+
timeoutMs: payload.timeoutMs === undefined ? 300000 : Number(payload.timeoutMs),
|
|
120
|
+
createdBy: req.user?.username || 'admin',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Schedule the job if enabled
|
|
124
|
+
if (doc.enabled) {
|
|
125
|
+
await cronScheduler.scheduleJob(doc);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
res.status(201).json({ item: doc.toObject() });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const safe = toSafeJsonError(err);
|
|
131
|
+
res.status(safe.status).json(safe.body);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
exports.updateCronJob = async (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const payload = req.body || {};
|
|
138
|
+
|
|
139
|
+
const doc = await CronJob.findById(req.params.id);
|
|
140
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
141
|
+
|
|
142
|
+
const wasEnabled = doc.enabled;
|
|
143
|
+
let needsReschedule = false;
|
|
144
|
+
|
|
145
|
+
if (payload.name !== undefined) doc.name = String(payload.name || '').trim();
|
|
146
|
+
if (payload.description !== undefined) doc.description = String(payload.description || '');
|
|
147
|
+
if (payload.cronExpression !== undefined) {
|
|
148
|
+
doc.cronExpression = String(payload.cronExpression || '').trim();
|
|
149
|
+
needsReschedule = true;
|
|
150
|
+
}
|
|
151
|
+
if (payload.timezone !== undefined) {
|
|
152
|
+
doc.timezone = String(payload.timezone || 'UTC');
|
|
153
|
+
needsReschedule = true;
|
|
154
|
+
}
|
|
155
|
+
if (payload.enabled !== undefined) {
|
|
156
|
+
doc.enabled = Boolean(payload.enabled);
|
|
157
|
+
needsReschedule = true;
|
|
158
|
+
}
|
|
159
|
+
if (payload.taskType !== undefined) {
|
|
160
|
+
const newTaskType = String(payload.taskType || '').trim();
|
|
161
|
+
|
|
162
|
+
// Validate task type requirements
|
|
163
|
+
if (newTaskType === 'script' && (!payload.scriptId || payload.scriptId === "")) {
|
|
164
|
+
return res.status(400).json({ error: 'Script ID is required for script-type cron jobs' });
|
|
165
|
+
}
|
|
166
|
+
if (newTaskType === 'http' && (!payload.httpUrl || payload.httpUrl.trim() === "")) {
|
|
167
|
+
return res.status(400).json({ error: 'URL is required for HTTP-type cron jobs' });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
doc.taskType = newTaskType;
|
|
171
|
+
}
|
|
172
|
+
if (payload.scriptId !== undefined) {
|
|
173
|
+
// Handle scriptId - convert empty string to null
|
|
174
|
+
let scriptId = payload.scriptId;
|
|
175
|
+
if (scriptId === "" || scriptId === null || scriptId === undefined) {
|
|
176
|
+
doc.scriptId = undefined;
|
|
177
|
+
} else {
|
|
178
|
+
doc.scriptId = scriptId;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (payload.scriptEnv !== undefined) doc.scriptEnv = normalizeEnv(payload.scriptEnv);
|
|
182
|
+
if (payload.httpMethod !== undefined) doc.httpMethod = String(payload.httpMethod || 'GET');
|
|
183
|
+
if (payload.httpUrl !== undefined) doc.httpUrl = String(payload.httpUrl || '').trim();
|
|
184
|
+
if (payload.httpHeaders !== undefined) doc.httpHeaders = normalizeHeaders(payload.httpHeaders);
|
|
185
|
+
if (payload.httpBody !== undefined) doc.httpBody = String(payload.httpBody || '');
|
|
186
|
+
if (payload.httpBodyType !== undefined) doc.httpBodyType = String(payload.httpBodyType || 'raw');
|
|
187
|
+
if (payload.httpAuth !== undefined) {
|
|
188
|
+
doc.httpAuth = {
|
|
189
|
+
type: String(payload.httpAuth?.type || 'none'),
|
|
190
|
+
token: String(payload.httpAuth?.token || ''),
|
|
191
|
+
username: String(payload.httpAuth?.username || ''),
|
|
192
|
+
password: String(payload.httpAuth?.password || ''),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (payload.timeoutMs !== undefined) doc.timeoutMs = Number(payload.timeoutMs || 0);
|
|
196
|
+
|
|
197
|
+
// Recalculate next run time if schedule changed
|
|
198
|
+
if (needsReschedule) {
|
|
199
|
+
if (doc.enabled) {
|
|
200
|
+
doc.nextRunAt = calculateNextRun(doc.cronExpression, doc.timezone);
|
|
201
|
+
} else {
|
|
202
|
+
doc.nextRunAt = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await doc.save();
|
|
207
|
+
|
|
208
|
+
// Update scheduler
|
|
209
|
+
if (needsReschedule) {
|
|
210
|
+
if (wasEnabled && !doc.enabled) {
|
|
211
|
+
await cronScheduler.unscheduleJob(doc._id);
|
|
212
|
+
} else if (!wasEnabled && doc.enabled) {
|
|
213
|
+
await cronScheduler.scheduleJob(doc);
|
|
214
|
+
} else if (wasEnabled && doc.enabled) {
|
|
215
|
+
await cronScheduler.unscheduleJob(doc._id);
|
|
216
|
+
await cronScheduler.scheduleJob(doc);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
res.json({ item: doc.toObject() });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
const safe = toSafeJsonError(err);
|
|
223
|
+
res.status(safe.status).json(safe.body);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
exports.deleteCronJob = async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
const doc = await CronJob.findById(req.params.id);
|
|
230
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
231
|
+
|
|
232
|
+
// Unschedule before deleting
|
|
233
|
+
if (doc.enabled) {
|
|
234
|
+
await cronScheduler.unscheduleJob(doc._id);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await CronJob.deleteOne({ _id: doc._id });
|
|
238
|
+
res.json({ deleted: true });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
const safe = toSafeJsonError(err);
|
|
241
|
+
res.status(safe.status).json(safe.body);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
exports.enableCronJob = async (req, res) => {
|
|
246
|
+
try {
|
|
247
|
+
const doc = await CronJob.findById(req.params.id);
|
|
248
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
249
|
+
|
|
250
|
+
if (!doc.enabled) {
|
|
251
|
+
doc.enabled = true;
|
|
252
|
+
doc.nextRunAt = calculateNextRun(doc.cronExpression, doc.timezone);
|
|
253
|
+
await doc.save();
|
|
254
|
+
await cronScheduler.scheduleJob(doc);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
res.json({ item: doc.toObject() });
|
|
258
|
+
} catch (err) {
|
|
259
|
+
const safe = toSafeJsonError(err);
|
|
260
|
+
res.status(safe.status).json(safe.body);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
exports.disableCronJob = async (req, res) => {
|
|
265
|
+
try {
|
|
266
|
+
const doc = await CronJob.findById(req.params.id);
|
|
267
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
268
|
+
|
|
269
|
+
if (doc.enabled) {
|
|
270
|
+
doc.enabled = false;
|
|
271
|
+
doc.nextRunAt = null;
|
|
272
|
+
await doc.save();
|
|
273
|
+
await cronScheduler.unscheduleJob(doc._id);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
res.json({ item: doc.toObject() });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
const safe = toSafeJsonError(err);
|
|
279
|
+
res.status(safe.status).json(safe.body);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
exports.triggerCronJob = async (req, res) => {
|
|
284
|
+
try {
|
|
285
|
+
const doc = await CronJob.findById(req.params.id);
|
|
286
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
287
|
+
|
|
288
|
+
// Execute immediately
|
|
289
|
+
const execution = await cronScheduler.executeJob(doc);
|
|
290
|
+
|
|
291
|
+
res.json({ executionId: execution._id });
|
|
292
|
+
} catch (err) {
|
|
293
|
+
const safe = toSafeJsonError(err);
|
|
294
|
+
res.status(safe.status).json(safe.body);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
exports.getExecutionHistory = async (req, res) => {
|
|
299
|
+
try {
|
|
300
|
+
const { page = 1, limit = 50 } = req.query;
|
|
301
|
+
const skip = (page - 1) * limit;
|
|
302
|
+
|
|
303
|
+
const items = await CronExecution.find({ cronJobId: req.params.id })
|
|
304
|
+
.sort({ startedAt: -1 })
|
|
305
|
+
.skip(skip)
|
|
306
|
+
.limit(parseInt(limit))
|
|
307
|
+
.lean();
|
|
308
|
+
|
|
309
|
+
const total = await CronExecution.countDocuments({ cronJobId: req.params.id });
|
|
310
|
+
|
|
311
|
+
res.json({
|
|
312
|
+
items,
|
|
313
|
+
pagination: {
|
|
314
|
+
page: parseInt(page),
|
|
315
|
+
limit: parseInt(limit),
|
|
316
|
+
total,
|
|
317
|
+
pages: Math.ceil(total / limit),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const safe = toSafeJsonError(err);
|
|
322
|
+
res.status(safe.status).json(safe.body);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
exports.getExecution = async (req, res) => {
|
|
327
|
+
try {
|
|
328
|
+
const doc = await CronExecution.findById(req.params.eid)
|
|
329
|
+
.populate('cronJobId', 'name taskType')
|
|
330
|
+
.lean();
|
|
331
|
+
if (!doc) return res.status(404).json({ error: 'Not found' });
|
|
332
|
+
res.json({ item: doc });
|
|
333
|
+
} catch (err) {
|
|
334
|
+
const safe = toSafeJsonError(err);
|
|
335
|
+
res.status(safe.status).json(safe.body);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
exports.getCronPresets = async (req, res) => {
|
|
340
|
+
try {
|
|
341
|
+
const presets = [
|
|
342
|
+
{ label: 'Every minute', expression: '* * * * *' },
|
|
343
|
+
{ label: 'Every 5 minutes', expression: '*/5 * * * *' },
|
|
344
|
+
{ label: 'Every 15 minutes', expression: '*/15 * * * *' },
|
|
345
|
+
{ label: 'Every 30 minutes', expression: '*/30 * * * *' },
|
|
346
|
+
{ label: 'Every hour', expression: '0 * * * *' },
|
|
347
|
+
{ label: 'Every 2 hours', expression: '0 */2 * * *' },
|
|
348
|
+
{ label: 'Every 6 hours', expression: '0 */6 * * *' },
|
|
349
|
+
{ label: 'Every day at midnight', expression: '0 0 * * *' },
|
|
350
|
+
{ label: 'Every day at 9 AM', expression: '0 9 * * *' },
|
|
351
|
+
{ label: 'Every Monday at 9 AM', expression: '0 9 * * 1' },
|
|
352
|
+
{ label: 'First day of month', expression: '0 0 1 * *' },
|
|
353
|
+
{ label: 'Weekdays only', expression: '0 9 * * 1-5' },
|
|
354
|
+
];
|
|
355
|
+
res.json({ presets });
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const safe = toSafeJsonError(err);
|
|
358
|
+
res.status(safe.status).json(safe.body);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
exports.previewNextRuns = async (req, res) => {
|
|
363
|
+
try {
|
|
364
|
+
const { cronExpression, timezone = 'UTC', count = 5 } = req.body;
|
|
365
|
+
|
|
366
|
+
if (!cronExpression) {
|
|
367
|
+
return res.status(400).json({ error: 'cronExpression is required' });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const runs = [];
|
|
371
|
+
const interval = parser.parseExpression(cronExpression, {
|
|
372
|
+
tz: timezone,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < parseInt(count); i++) {
|
|
376
|
+
try {
|
|
377
|
+
runs.push(interval.next().toDate());
|
|
378
|
+
} catch (err) {
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
res.json({ runs });
|
|
384
|
+
} catch (err) {
|
|
385
|
+
const safe = toSafeJsonError(err);
|
|
386
|
+
res.status(safe.status).json(safe.body);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const dbBrowser = require('../services/dbBrowser.service');
|
|
2
|
+
|
|
3
|
+
exports.listConnections = async (req, res) => {
|
|
4
|
+
try {
|
|
5
|
+
const items = await dbBrowser.listConnections();
|
|
6
|
+
res.json({ items });
|
|
7
|
+
} catch (err) {
|
|
8
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
9
|
+
res.status(safe.status).json(safe.body);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
exports.getConnection = async (req, res) => {
|
|
14
|
+
try {
|
|
15
|
+
const item = await dbBrowser.getConnection(req.params.id);
|
|
16
|
+
res.json({ item });
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
19
|
+
res.status(safe.status).json(safe.body);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
exports.createConnection = async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const item = await dbBrowser.createConnection(req.body || {});
|
|
26
|
+
res.status(201).json({ item });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err?.code === 11000) {
|
|
29
|
+
return res.status(400).json({ error: 'Connection name must be unique' });
|
|
30
|
+
}
|
|
31
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
32
|
+
res.status(safe.status).json(safe.body);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
exports.updateConnection = async (req, res) => {
|
|
37
|
+
try {
|
|
38
|
+
const item = await dbBrowser.updateConnection(req.params.id, req.body || {});
|
|
39
|
+
res.json({ item });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err?.code === 11000) {
|
|
42
|
+
return res.status(400).json({ error: 'Connection name must be unique' });
|
|
43
|
+
}
|
|
44
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
45
|
+
res.status(safe.status).json(safe.body);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
exports.deleteConnection = async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const out = await dbBrowser.deleteConnection(req.params.id);
|
|
52
|
+
res.json(out);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
55
|
+
res.status(safe.status).json(safe.body);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
exports.testConnection = async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const out = await dbBrowser.testConnection(req.params.id);
|
|
62
|
+
res.json(out);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
65
|
+
res.status(safe.status).json(safe.body);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
exports.listDatabases = async (req, res) => {
|
|
70
|
+
try {
|
|
71
|
+
const items = await dbBrowser.listDatabases(req.params.id);
|
|
72
|
+
res.json({ items });
|
|
73
|
+
} catch (err) {
|
|
74
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
75
|
+
res.status(safe.status).json(safe.body);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
exports.listNamespaces = async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const items = await dbBrowser.listNamespaces(req.params.id, req.params.database);
|
|
82
|
+
res.json({ items });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
85
|
+
res.status(safe.status).json(safe.body);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
exports.getSchema = async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const schema = await dbBrowser.getSchema(req.params.id, req.params.database, req.params.namespace);
|
|
92
|
+
res.json({ schema });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
95
|
+
res.status(safe.status).json(safe.body);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
exports.listRecords = async (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const out = await dbBrowser.listRecords(req.params.id, req.params.database, req.params.namespace, {
|
|
102
|
+
page: req.query.page,
|
|
103
|
+
pageSize: req.query.pageSize,
|
|
104
|
+
filterField: req.query.filterField,
|
|
105
|
+
filterValue: req.query.filterValue,
|
|
106
|
+
sortField: req.query.sortField,
|
|
107
|
+
sortOrder: req.query.sortOrder,
|
|
108
|
+
});
|
|
109
|
+
res.json(out);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
112
|
+
res.status(safe.status).json(safe.body);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
exports.getRecord = async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const item = await dbBrowser.getRecord(req.params.id, req.params.database, req.params.namespace, req.params.recordId);
|
|
119
|
+
res.json({ item });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const safe = dbBrowser.toSafeJsonError(err);
|
|
122
|
+
res.status(safe.status).json(safe.body);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
@@ -83,10 +83,21 @@ exports.getFile = async (req, res) => {
|
|
|
83
83
|
const viewsRoot = normalizeViewsRoot(req);
|
|
84
84
|
const relPath = ejsVirtualService.normalizeRelPath(String(req.query.path || '').trim());
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
let fsContent = '';
|
|
87
|
+
try {
|
|
88
|
+
fsContent = await ejsVirtualService.readFsView(viewsRoot, relPath);
|
|
89
|
+
} catch (_) {
|
|
90
|
+
fsContent = '';
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
const override = await VirtualEjsFile.findOne({ path: relPath }).lean();
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
let effective;
|
|
96
|
+
try {
|
|
97
|
+
effective = await ejsVirtualService.resolveTemplateSource({ viewsRoot, relPath, allowDb: true });
|
|
98
|
+
} catch (_) {
|
|
99
|
+
effective = { source: override ? 'db' : 'none', content: override?.content || '' };
|
|
100
|
+
}
|
|
90
101
|
|
|
91
102
|
res.json({
|
|
92
103
|
path: relPath,
|
|
@@ -97,7 +108,6 @@ exports.getFile = async (req, res) => {
|
|
|
97
108
|
} catch (err) {
|
|
98
109
|
const code = err.code;
|
|
99
110
|
if (code === 'VALIDATION') return res.status(400).json({ error: err.message });
|
|
100
|
-
if (code === 'NOT_FOUND') return res.status(404).json({ error: err.message });
|
|
101
111
|
console.error('[adminEjsVirtual] getFile error', err);
|
|
102
112
|
res.status(500).json({ error: 'Failed to load file' });
|
|
103
113
|
}
|