@intranefr/superbackend 1.5.1 → 1.5.3
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 +10 -0
- package/index.js +2 -0
- package/manage.js +745 -0
- package/package.json +5 -2
- package/src/controllers/admin.controller.js +79 -6
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminLlm.controller.js +19 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminScripts.controller.js +243 -74
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/markdowns.controller.js +42 -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.js +195 -34
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/CacheEntry.js +1 -1
- package/src/models/ConsoleLog.js +1 -1
- 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/GlobalSetting.js +1 -2
- package/src/models/Markdown.js +75 -0
- package/src/models/RateLimitCounter.js +1 -1
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminConsoleManager.routes.js +1 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminLlm.routes.js +1 -0
- package/src/routes/adminMarkdowns.routes.js +16 -0
- package/src/routes/adminScripts.routes.js +4 -1
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/markdowns.routes.js +16 -0
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/blogCronsBootstrap.service.js +7 -6
- package/src/services/consoleManager.service.js +56 -18
- package/src/services/consoleOverride.service.js +1 -0
- 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/globalSettings.service.js +15 -0
- package/src/services/jsonConfigs.service.js +24 -12
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/scriptsRunner.service.js +514 -23
- package/src/services/telegram.service.js +130 -0
- package/src/utils/rbac/rightsRegistry.js +4 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard.ejs +63 -12
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-scripts.ejs +817 -6
- package/views/admin-telegram.ejs +269 -0
- package/views/partials/dashboard/nav-items.ejs +4 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/src/middleware/internalCronAuth.js +0 -29
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
const GlobalSetting = require('../models/GlobalSetting');
|
|
4
|
+
const CronJob = require('../models/CronJob');
|
|
5
|
+
|
|
6
|
+
const globalSettingsService = require('./globalSettings.service');
|
|
7
|
+
|
|
8
|
+
const INTERNAL_CRON_TOKEN_SETTING_KEY = 'experiments.internalCronToken';
|
|
9
|
+
|
|
10
|
+
const CRON_NAME_AGGREGATE = 'Experiments: Aggregate + Evaluate Winner';
|
|
11
|
+
const CRON_NAME_RETENTION = 'Experiments: Retention Cleanup';
|
|
12
|
+
|
|
13
|
+
function getDefaultInternalCronToken() {
|
|
14
|
+
return crypto.randomBytes(24).toString('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function ensureSettingExists({ key, type, description, defaultValue }) {
|
|
18
|
+
const existing = await GlobalSetting.findOne({ key }).lean();
|
|
19
|
+
if (existing) return existing;
|
|
20
|
+
|
|
21
|
+
const doc = await GlobalSetting.create({
|
|
22
|
+
key,
|
|
23
|
+
type,
|
|
24
|
+
description,
|
|
25
|
+
value: type === 'json' ? JSON.stringify(defaultValue) : String(defaultValue ?? ''),
|
|
26
|
+
templateVariables: [],
|
|
27
|
+
public: false,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
globalSettingsService.clearSettingsCache();
|
|
31
|
+
return doc.toObject();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function ensureInternalTokenExists() {
|
|
35
|
+
await ensureSettingExists({
|
|
36
|
+
key: INTERNAL_CRON_TOKEN_SETTING_KEY,
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'Bearer token used by CronJobs to call internal experiments endpoints.',
|
|
39
|
+
defaultValue: getDefaultInternalCronToken(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const raw = await globalSettingsService.getSettingValue(INTERNAL_CRON_TOKEN_SETTING_KEY, '');
|
|
43
|
+
return String(raw || '').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function ensureCronJobs({ baseUrl }) {
|
|
47
|
+
const aggregateUrl = `${baseUrl}/api/internal/experiments/aggregate/run`;
|
|
48
|
+
const retentionUrl = `${baseUrl}/api/internal/experiments/retention/run`;
|
|
49
|
+
|
|
50
|
+
// Use the same Basic Auth credentials as the admin API.
|
|
51
|
+
// This keeps a single source of truth and avoids CronJobs drifting to other env vars.
|
|
52
|
+
const internalCronUsername = process.env.ADMIN_USERNAME || 'admin';
|
|
53
|
+
const internalCronPassword = process.env.ADMIN_PASSWORD || 'admin';
|
|
54
|
+
|
|
55
|
+
const aggDoc = {
|
|
56
|
+
name: CRON_NAME_AGGREGATE,
|
|
57
|
+
description: 'Aggregates experiment events into buckets and evaluates winners.',
|
|
58
|
+
cronExpression: '*/15 * * * *',
|
|
59
|
+
timezone: 'UTC',
|
|
60
|
+
enabled: true,
|
|
61
|
+
nextRunAt: null,
|
|
62
|
+
taskType: 'http',
|
|
63
|
+
httpMethod: 'POST',
|
|
64
|
+
httpUrl: aggregateUrl,
|
|
65
|
+
httpHeaders: [],
|
|
66
|
+
httpBody: JSON.stringify({}),
|
|
67
|
+
httpBodyType: 'json',
|
|
68
|
+
httpAuth: { type: 'basic', username: internalCronUsername, password: internalCronPassword },
|
|
69
|
+
timeoutMs: 5 * 60 * 1000,
|
|
70
|
+
createdBy: 'system',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await CronJob.updateOne(
|
|
74
|
+
{ name: CRON_NAME_AGGREGATE, taskType: 'http' },
|
|
75
|
+
{ $set: aggDoc, $setOnInsert: { createdAt: new Date() } },
|
|
76
|
+
{ upsert: true },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const retentionDoc = {
|
|
80
|
+
name: CRON_NAME_RETENTION,
|
|
81
|
+
description: 'Deletes old experiment events and metric buckets based on retention settings.',
|
|
82
|
+
cronExpression: '0 3 * * *',
|
|
83
|
+
timezone: 'UTC',
|
|
84
|
+
enabled: true,
|
|
85
|
+
nextRunAt: null,
|
|
86
|
+
taskType: 'http',
|
|
87
|
+
httpMethod: 'POST',
|
|
88
|
+
httpUrl: retentionUrl,
|
|
89
|
+
httpHeaders: [],
|
|
90
|
+
httpBody: JSON.stringify({}),
|
|
91
|
+
httpBodyType: 'json',
|
|
92
|
+
httpAuth: { type: 'basic', username: internalCronUsername, password: internalCronPassword },
|
|
93
|
+
timeoutMs: 10 * 60 * 1000,
|
|
94
|
+
createdBy: 'system',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await CronJob.updateOne(
|
|
98
|
+
{ name: CRON_NAME_RETENTION, taskType: 'http' },
|
|
99
|
+
{ $set: retentionDoc, $setOnInsert: { createdAt: new Date() } },
|
|
100
|
+
{ upsert: true },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function bootstrap() {
|
|
105
|
+
// CronScheduler HTTP jobs need an absolute base URL.
|
|
106
|
+
const baseUrl =
|
|
107
|
+
String(process.env.SUPERBACKEND_BASE_URL || process.env.PUBLIC_URL || '').trim() ||
|
|
108
|
+
'http://localhost:3000';
|
|
109
|
+
|
|
110
|
+
await ensureCronJobs({ baseUrl: baseUrl.replace(/\/+$/, '') });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
bootstrap,
|
|
115
|
+
INTERNAL_CRON_TOKEN_SETTING_KEY,
|
|
116
|
+
CRON_NAME_AGGREGATE,
|
|
117
|
+
CRON_NAME_RETENTION,
|
|
118
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const globalSettingsService = require('./globalSettings.service');
|
|
2
|
+
|
|
3
|
+
const ExperimentEvent = require('../models/ExperimentEvent');
|
|
4
|
+
const ExperimentMetricBucket = require('../models/ExperimentMetricBucket');
|
|
5
|
+
|
|
6
|
+
function toInt(val, fallback) {
|
|
7
|
+
const n = parseInt(String(val), 10);
|
|
8
|
+
return Number.isFinite(n) ? n : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runRetentionCleanup() {
|
|
12
|
+
const eventsRetentionDays = toInt(
|
|
13
|
+
await globalSettingsService.getSettingValue('EXPERIMENT_EVENTS_RETENTION_DAYS', '30'),
|
|
14
|
+
30,
|
|
15
|
+
);
|
|
16
|
+
const metricsRetentionDays = toInt(
|
|
17
|
+
await globalSettingsService.getSettingValue('EXPERIMENT_METRICS_RETENTION_DAYS', '180'),
|
|
18
|
+
180,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const eventsCutoff = new Date(now - eventsRetentionDays * 24 * 60 * 60 * 1000);
|
|
23
|
+
const metricsCutoff = new Date(now - metricsRetentionDays * 24 * 60 * 60 * 1000);
|
|
24
|
+
|
|
25
|
+
const [eventsRes, bucketsRes] = await Promise.all([
|
|
26
|
+
ExperimentEvent.deleteMany({ ts: { $lt: eventsCutoff } }),
|
|
27
|
+
ExperimentMetricBucket.deleteMany({ bucketStart: { $lt: metricsCutoff } }),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
eventsRetentionDays,
|
|
32
|
+
metricsRetentionDays,
|
|
33
|
+
cutoffs: { eventsCutoff: eventsCutoff.toISOString(), metricsCutoff: metricsCutoff.toISOString() },
|
|
34
|
+
deleted: {
|
|
35
|
+
events: eventsRes?.deletedCount ?? 0,
|
|
36
|
+
metricBuckets: bucketsRes?.deletedCount ?? 0,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
runRetentionCleanup,
|
|
43
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const { WebSocketServer } = require('ws');
|
|
2
|
+
const url = require('url');
|
|
3
|
+
|
|
4
|
+
const subscribersByCode = new Map(); // experimentCode -> Set<ws>
|
|
5
|
+
|
|
6
|
+
function safeSend(ws, payload) {
|
|
7
|
+
try {
|
|
8
|
+
ws.send(JSON.stringify(payload));
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeCode(v) {
|
|
14
|
+
return String(v || '').trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function broadcastWinnerChanged({ experimentCode, winnerVariantKey, decidedAt }) {
|
|
18
|
+
const code = normalizeCode(experimentCode);
|
|
19
|
+
if (!code) return;
|
|
20
|
+
|
|
21
|
+
const subs = subscribersByCode.get(code);
|
|
22
|
+
if (!subs || subs.size === 0) return;
|
|
23
|
+
|
|
24
|
+
const msg = {
|
|
25
|
+
type: 'winner',
|
|
26
|
+
experimentCode: code,
|
|
27
|
+
winnerVariantKey: winnerVariantKey || null,
|
|
28
|
+
decidedAt: decidedAt ? new Date(decidedAt).toISOString() : null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const ws of subs) {
|
|
32
|
+
if (ws.readyState !== ws.OPEN) continue;
|
|
33
|
+
safeSend(ws, msg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function attachExperimentsWebsocketServer(server) {
|
|
38
|
+
const wsPath = '/api/experiments/ws';
|
|
39
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
40
|
+
|
|
41
|
+
server.on('upgrade', (req, socket, head) => {
|
|
42
|
+
const parsed = url.parse(req.url, true);
|
|
43
|
+
if (!parsed || parsed.pathname !== wsPath) return;
|
|
44
|
+
|
|
45
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
46
|
+
wss.emit('connection', ws, req, parsed);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
wss.on('connection', (ws, _req, parsed) => {
|
|
51
|
+
ws._sbExperimentSubs = new Set();
|
|
52
|
+
|
|
53
|
+
safeSend(ws, { type: 'hello' });
|
|
54
|
+
|
|
55
|
+
ws.on('message', (raw) => {
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(String(raw || ''));
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const type = normalizeCode(msg?.type);
|
|
64
|
+
const experimentCode = normalizeCode(msg?.experimentCode);
|
|
65
|
+
|
|
66
|
+
if (type === 'subscribe') {
|
|
67
|
+
if (!experimentCode) return;
|
|
68
|
+
|
|
69
|
+
let set = subscribersByCode.get(experimentCode);
|
|
70
|
+
if (!set) {
|
|
71
|
+
set = new Set();
|
|
72
|
+
subscribersByCode.set(experimentCode, set);
|
|
73
|
+
}
|
|
74
|
+
set.add(ws);
|
|
75
|
+
ws._sbExperimentSubs.add(experimentCode);
|
|
76
|
+
safeSend(ws, { type: 'subscribed', experimentCode });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (type === 'unsubscribe') {
|
|
81
|
+
if (!experimentCode) return;
|
|
82
|
+
|
|
83
|
+
const set = subscribersByCode.get(experimentCode);
|
|
84
|
+
if (set) {
|
|
85
|
+
set.delete(ws);
|
|
86
|
+
if (set.size === 0) subscribersByCode.delete(experimentCode);
|
|
87
|
+
}
|
|
88
|
+
ws._sbExperimentSubs.delete(experimentCode);
|
|
89
|
+
safeSend(ws, { type: 'unsubscribed', experimentCode });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
ws.on('close', () => {
|
|
94
|
+
for (const code of ws._sbExperimentSubs || []) {
|
|
95
|
+
const set = subscribersByCode.get(code);
|
|
96
|
+
if (set) {
|
|
97
|
+
set.delete(ws);
|
|
98
|
+
if (set.size === 0) subscribersByCode.delete(code);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ws.on('error', () => {
|
|
104
|
+
for (const code of ws._sbExperimentSubs || []) {
|
|
105
|
+
const set = subscribersByCode.get(code);
|
|
106
|
+
if (set) {
|
|
107
|
+
set.delete(ws);
|
|
108
|
+
if (set.size === 0) subscribersByCode.delete(code);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// If query provides experimentCode, auto-subscribe.
|
|
114
|
+
const q = parsed && parsed.query ? parsed.query : {};
|
|
115
|
+
const initial = normalizeCode(q.experimentCode);
|
|
116
|
+
if (initial) {
|
|
117
|
+
let set = subscribersByCode.get(initial);
|
|
118
|
+
if (!set) {
|
|
119
|
+
set = new Set();
|
|
120
|
+
subscribersByCode.set(initial, set);
|
|
121
|
+
}
|
|
122
|
+
set.add(ws);
|
|
123
|
+
ws._sbExperimentSubs.add(initial);
|
|
124
|
+
safeSend(ws, { type: 'subscribed', experimentCode: initial });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return { wss, wsPath };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
attachExperimentsWebsocketServer,
|
|
133
|
+
broadcastWinnerChanged,
|
|
134
|
+
};
|
|
@@ -39,11 +39,26 @@ async function getSettingValue(key, defaultValue = null) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
async function deleteSetting(key) {
|
|
43
|
+
try {
|
|
44
|
+
const setting = await GlobalSetting.findOneAndDelete({ key });
|
|
45
|
+
|
|
46
|
+
// Clear cache for this key
|
|
47
|
+
settingsCache.delete(key);
|
|
48
|
+
|
|
49
|
+
return setting;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`Error deleting setting ${key}:`, error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
function clearSettingsCache() {
|
|
43
57
|
settingsCache.clear();
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
module.exports = {
|
|
47
61
|
getSettingValue,
|
|
62
|
+
deleteSetting,
|
|
48
63
|
clearSettingsCache,
|
|
49
64
|
};
|
|
@@ -2,6 +2,18 @@ const crypto = require('crypto');
|
|
|
2
2
|
|
|
3
3
|
const JsonConfig = require('../models/JsonConfig');
|
|
4
4
|
|
|
5
|
+
const logger = {
|
|
6
|
+
log: (...args) => {
|
|
7
|
+
if (process.env.DEBUG_JSON_CONFIGS === 'true' && !process.env.TUI_MODE) console.log(...args);
|
|
8
|
+
},
|
|
9
|
+
warn: (...args) => {
|
|
10
|
+
if (process.env.DEBUG_JSON_CONFIGS === 'true' && !process.env.TUI_MODE) console.warn(...args);
|
|
11
|
+
},
|
|
12
|
+
error: (...args) => {
|
|
13
|
+
console.error(...args);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
5
17
|
const cache = new Map();
|
|
6
18
|
|
|
7
19
|
function normalizeSlugBase(title) {
|
|
@@ -122,7 +134,7 @@ async function getJsonConfigById(id) {
|
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTtlSeconds = 0, alias }) {
|
|
125
|
-
|
|
137
|
+
logger.log('createJsonConfig called with:', { title, jsonRaw, publicEnabled, cacheTtlSeconds, alias });
|
|
126
138
|
|
|
127
139
|
const normalizedTitle = String(title || '').trim();
|
|
128
140
|
if (!normalizedTitle) {
|
|
@@ -142,7 +154,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
|
|
|
142
154
|
let normalizedAlias = null;
|
|
143
155
|
if (alias !== undefined && alias !== null) {
|
|
144
156
|
normalizedAlias = normalizeAlias(alias);
|
|
145
|
-
|
|
157
|
+
logger.log('Normalized alias:', normalizedAlias);
|
|
146
158
|
if (normalizedAlias && !(await validateAliasUniqueness(normalizedAlias))) {
|
|
147
159
|
const err = new Error('Alias must be unique and not conflict with existing slugs or aliases');
|
|
148
160
|
err.code = 'ALIAS_NOT_UNIQUE';
|
|
@@ -162,10 +174,10 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
|
|
|
162
174
|
jsonHash: computeJsonHash(String(jsonRaw)),
|
|
163
175
|
};
|
|
164
176
|
|
|
165
|
-
console.log('Creating document with data:', createData);
|
|
177
|
+
//console.log('Creating document with data:', createData);
|
|
166
178
|
|
|
167
179
|
const doc = await JsonConfig.create(createData);
|
|
168
|
-
console.log('Created document:', doc.toObject());
|
|
180
|
+
//console.log('Created document:', doc.toObject());
|
|
169
181
|
|
|
170
182
|
clearJsonConfigCache(slug);
|
|
171
183
|
if (normalizedAlias) {
|
|
@@ -175,7 +187,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
|
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
async function updateJsonConfig(id, patch) {
|
|
178
|
-
|
|
190
|
+
logger.log('updateJsonConfig called with id:', id, 'patch:', patch);
|
|
179
191
|
|
|
180
192
|
const doc = await JsonConfig.findById(id);
|
|
181
193
|
if (!doc) {
|
|
@@ -184,7 +196,7 @@ async function updateJsonConfig(id, patch) {
|
|
|
184
196
|
throw err;
|
|
185
197
|
}
|
|
186
198
|
|
|
187
|
-
|
|
199
|
+
logger.log('Found document:', doc.toObject());
|
|
188
200
|
|
|
189
201
|
const oldSlug = doc.slug;
|
|
190
202
|
const oldAlias = doc.alias;
|
|
@@ -222,14 +234,14 @@ async function updateJsonConfig(id, patch) {
|
|
|
222
234
|
|
|
223
235
|
if (patch && Object.prototype.hasOwnProperty.call(patch, 'alias')) {
|
|
224
236
|
const newAlias = patch.alias;
|
|
225
|
-
|
|
237
|
+
logger.log('Processing alias update. newAlias:', newAlias);
|
|
226
238
|
|
|
227
239
|
if (newAlias === null || newAlias === undefined || newAlias === '') {
|
|
228
240
|
doc.alias = undefined;
|
|
229
|
-
|
|
241
|
+
logger.log('Setting alias to undefined');
|
|
230
242
|
} else {
|
|
231
243
|
const normalizedAlias = normalizeAlias(newAlias);
|
|
232
|
-
|
|
244
|
+
logger.log('Normalized alias for update:', normalizedAlias);
|
|
233
245
|
|
|
234
246
|
if (!normalizedAlias) {
|
|
235
247
|
const err = new Error('Invalid alias format');
|
|
@@ -244,7 +256,7 @@ async function updateJsonConfig(id, patch) {
|
|
|
244
256
|
}
|
|
245
257
|
|
|
246
258
|
doc.alias = normalizedAlias;
|
|
247
|
-
|
|
259
|
+
logger.log('Setting alias to:', normalizedAlias);
|
|
248
260
|
}
|
|
249
261
|
}
|
|
250
262
|
|
|
@@ -252,9 +264,9 @@ async function updateJsonConfig(id, patch) {
|
|
|
252
264
|
doc.slug = await generateUniqueSlugFromTitle(doc.title);
|
|
253
265
|
}
|
|
254
266
|
|
|
255
|
-
|
|
267
|
+
logger.log('Document before save:', doc.toObject());
|
|
256
268
|
await doc.save();
|
|
257
|
-
|
|
269
|
+
logger.log('Document after save:', doc.toObject());
|
|
258
270
|
|
|
259
271
|
clearJsonConfigCache(oldSlug);
|
|
260
272
|
clearJsonConfigCache(doc.slug);
|