@intranefr/superbackend 1.5.2 → 1.6.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/cookies.txt +6 -0
- package/cookies1.txt +6 -0
- package/cookies2.txt +6 -0
- package/cookies3.txt +6 -0
- package/cookies4.txt +5 -0
- package/cookies_old.txt +5 -0
- package/cookies_old_test.txt +6 -0
- package/cookies_super.txt +5 -0
- package/cookies_super_test.txt +6 -0
- package/cookies_test.txt +6 -0
- package/index.js +9 -0
- package/manage.js +745 -0
- package/package.json +6 -2
- package/plugins/core-waiting-list-migration/README.md +118 -0
- package/plugins/core-waiting-list-migration/index.js +438 -0
- package/plugins/global-settings-presets/index.js +20 -0
- package/plugins/hello-cli/index.js +17 -0
- package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
- package/plugins/ui-components-seeder/components/suiToast.js +186 -0
- package/plugins/ui-components-seeder/index.js +31 -0
- package/public/js/admin-ui-components-preview.js +281 -0
- package/public/js/admin-ui-components.js +408 -0
- package/public/js/llm-provider-model-picker.js +193 -0
- package/public/test-iframe-fix.html +63 -0
- package/public/test-iframe.html +14 -0
- package/src/admin/endpointRegistry.js +68 -0
- package/src/controllers/admin.controller.js +36 -10
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +19 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminScripts.controller.js +138 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/helpers/mongooseHelper.js +6 -6
- package/src/helpers/scriptBase.js +2 -2
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +584 -176
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/Markdown.js +75 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/UiComponent.js +2 -0
- package/src/models/User.js +1 -1
- package/src/routes/admin.routes.js +3 -3
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminAssets.routes.js +11 -11
- package/src/routes/adminBlog.routes.js +2 -2
- package/src/routes/adminBlogAi.routes.js +2 -2
- package/src/routes/adminBlogAutomation.routes.js +2 -2
- package/src/routes/adminCache.routes.js +2 -2
- package/src/routes/adminConsoleManager.routes.js +2 -2
- package/src/routes/adminCrons.routes.js +2 -2
- package/src/routes/adminDataCleanup.routes.js +26 -0
- package/src/routes/adminDbBrowser.routes.js +2 -2
- package/src/routes/adminEjsVirtual.routes.js +2 -2
- package/src/routes/adminFeatureFlags.routes.js +6 -6
- package/src/routes/adminHeadless.routes.js +2 -2
- package/src/routes/adminHealthChecks.routes.js +2 -2
- package/src/routes/adminI18n.routes.js +2 -2
- package/src/routes/adminJsonConfigs.routes.js +8 -8
- package/src/routes/adminLlm.routes.js +8 -7
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +10 -0
- package/src/routes/adminMigration.routes.js +12 -12
- package/src/routes/adminPages.routes.js +2 -2
- package/src/routes/adminPlugins.routes.js +15 -0
- package/src/routes/adminProxy.routes.js +2 -2
- package/src/routes/adminRateLimits.routes.js +8 -8
- package/src/routes/adminRbac.routes.js +2 -2
- package/src/routes/adminRegistry.routes.js +24 -0
- package/src/routes/adminScripts.routes.js +6 -3
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/adminTerminals.routes.js +2 -2
- package/src/routes/adminUiComponents.routes.js +2 -2
- package/src/routes/adminUploadNamespaces.routes.js +7 -7
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +2 -2
- package/src/routes/formsAdmin.routes.js +6 -6
- package/src/routes/globalSettings.routes.js +8 -8
- package/src/routes/internalExperiments.routes.js +2 -2
- package/src/routes/markdowns.routes.js +16 -0
- package/src/routes/notificationAdmin.routes.js +7 -7
- package/src/routes/orgAdmin.routes.js +16 -16
- package/src/routes/pages.routes.js +3 -3
- package/src/routes/registry.routes.js +11 -0
- package/src/routes/stripeAdmin.routes.js +12 -12
- package/src/routes/userAdmin.routes.js +7 -7
- package/src/routes/waitingListAdmin.routes.js +2 -2
- package/src/routes/workflows.routes.js +3 -3
- 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/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +284 -10
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/scriptsRunner.service.js +328 -37
- package/src/services/telegram.service.js +130 -0
- package/src/services/uiComponents.service.js +180 -0
- package/src/services/waitingListJson.service.js +401 -0
- package/src/utils/rbac/rightsRegistry.js +118 -0
- package/test-access.js +63 -0
- package/test-iframe-fix.html +63 -0
- package/test-iframe.html +14 -0
- package/views/admin-403.ejs +92 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +179 -7
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-experiments.ejs +1 -1
- package/views/admin-login.ejs +286 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-scripts.ejs +221 -4
- package/views/admin-telegram.ejs +269 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +5 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
- package/analysis-only.skill +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
function toSafeJsonError(error) {
|
|
4
|
+
const msg = error?.message || 'Operation failed';
|
|
5
|
+
const code = error?.code;
|
|
6
|
+
if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
|
|
7
|
+
if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
|
|
8
|
+
if (code === 'FORBIDDEN') return { status: 403, body: { error: msg } };
|
|
9
|
+
return { status: 500, body: { error: msg } };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureDbConnection() {
|
|
13
|
+
const db = mongoose.connection?.db;
|
|
14
|
+
if (!db) {
|
|
15
|
+
throw Object.assign(new Error('MongoDB connection is not ready'), { code: 'FORBIDDEN' });
|
|
16
|
+
}
|
|
17
|
+
return db;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeCollectionName(value) {
|
|
21
|
+
const name = String(value || '').trim();
|
|
22
|
+
if (!name) throw Object.assign(new Error('collection is required'), { code: 'VALIDATION' });
|
|
23
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name)) {
|
|
24
|
+
throw Object.assign(new Error('Invalid collection name'), { code: 'VALIDATION' });
|
|
25
|
+
}
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeDateField(value, fallback = 'createdAt') {
|
|
30
|
+
const field = String(value || fallback).trim();
|
|
31
|
+
if (!field) throw Object.assign(new Error('dateField is required'), { code: 'VALIDATION' });
|
|
32
|
+
if (field.includes('$')) {
|
|
33
|
+
throw Object.assign(new Error('Invalid dateField'), { code: 'VALIDATION' });
|
|
34
|
+
}
|
|
35
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(field)) {
|
|
36
|
+
throw Object.assign(new Error('Invalid dateField'), { code: 'VALIDATION' });
|
|
37
|
+
}
|
|
38
|
+
return field;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeOlderThanDays(value) {
|
|
42
|
+
const n = Number(value);
|
|
43
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
44
|
+
throw Object.assign(new Error('olderThanDays must be a positive number'), { code: 'VALIDATION' });
|
|
45
|
+
}
|
|
46
|
+
if (n > 36500) {
|
|
47
|
+
throw Object.assign(new Error('olderThanDays is too large'), { code: 'VALIDATION' });
|
|
48
|
+
}
|
|
49
|
+
return Math.floor(n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeLimit(value, fallback = 5000) {
|
|
53
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
54
|
+
const n = Number(value);
|
|
55
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
56
|
+
throw Object.assign(new Error('limit must be a positive number'), { code: 'VALIDATION' });
|
|
57
|
+
}
|
|
58
|
+
return Math.min(Math.floor(n), 50000);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getCollectionStatsByName(db, collection) {
|
|
62
|
+
const command = await db.command({ collStats: collection });
|
|
63
|
+
return {
|
|
64
|
+
name: collection,
|
|
65
|
+
ns: command?.ns || null,
|
|
66
|
+
count: Number(command?.count || 0),
|
|
67
|
+
sizeBytes: Number(command?.size || 0),
|
|
68
|
+
storageSizeBytes: Number(command?.storageSize || 0),
|
|
69
|
+
totalIndexSizeBytes: Number(command?.totalIndexSize || 0),
|
|
70
|
+
avgObjSizeBytes: Number(command?.avgObjSize || 0),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function listCollectionStats() {
|
|
75
|
+
const db = ensureDbConnection();
|
|
76
|
+
const collections = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
77
|
+
const names = (collections || []).map((c) => c.name).filter(Boolean).sort();
|
|
78
|
+
|
|
79
|
+
const stats = [];
|
|
80
|
+
for (const name of names) {
|
|
81
|
+
try {
|
|
82
|
+
// collStats can fail on internal collections depending on Mongo version/config.
|
|
83
|
+
const s = await getCollectionStatsByName(db, name);
|
|
84
|
+
stats.push(s);
|
|
85
|
+
} catch {
|
|
86
|
+
stats.push({
|
|
87
|
+
name,
|
|
88
|
+
ns: null,
|
|
89
|
+
count: 0,
|
|
90
|
+
sizeBytes: 0,
|
|
91
|
+
storageSizeBytes: 0,
|
|
92
|
+
totalIndexSizeBytes: 0,
|
|
93
|
+
avgObjSizeBytes: 0,
|
|
94
|
+
unavailable: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return stats;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function getMongoGlobalStats() {
|
|
103
|
+
const db = ensureDbConnection();
|
|
104
|
+
const stats = await db.stats();
|
|
105
|
+
return {
|
|
106
|
+
db: stats?.db || null,
|
|
107
|
+
collections: Number(stats?.collections || 0),
|
|
108
|
+
views: Number(stats?.views || 0),
|
|
109
|
+
objects: Number(stats?.objects || 0),
|
|
110
|
+
dataSizeBytes: Number(stats?.dataSize || 0),
|
|
111
|
+
storageSizeBytes: Number(stats?.storageSize || 0),
|
|
112
|
+
indexes: Number(stats?.indexes || 0),
|
|
113
|
+
indexSizeBytes: Number(stats?.indexSize || 0),
|
|
114
|
+
totalSizeBytes: Number(stats?.totalSize || 0),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function getOverviewStats() {
|
|
119
|
+
const [global, collections] = await Promise.all([
|
|
120
|
+
getMongoGlobalStats(),
|
|
121
|
+
listCollectionStats(),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
return { global, collections };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function ensureCollectionExists(db, collectionName) {
|
|
128
|
+
const collections = await db.listCollections({ name: collectionName }, { nameOnly: true }).toArray();
|
|
129
|
+
if (!collections || collections.length === 0) {
|
|
130
|
+
throw Object.assign(new Error('Collection not found'), { code: 'NOT_FOUND' });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function buildOlderThanQuery({ dateField, cutoff }) {
|
|
135
|
+
return {
|
|
136
|
+
[dateField]: {
|
|
137
|
+
$type: 'date',
|
|
138
|
+
$lt: cutoff,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function estimateReclaimableBytes({ candidateCount, collectionStats }) {
|
|
144
|
+
const avg = Number(collectionStats?.avgObjSizeBytes || 0);
|
|
145
|
+
if (avg > 0) return Math.round(candidateCount * avg);
|
|
146
|
+
|
|
147
|
+
const sizeBytes = Number(collectionStats?.sizeBytes || 0);
|
|
148
|
+
const count = Number(collectionStats?.count || 0);
|
|
149
|
+
if (sizeBytes > 0 && count > 0) {
|
|
150
|
+
return Math.round(candidateCount * (sizeBytes / count));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function dryRunCollectionCleanup({ collection, olderThanDays, dateField = 'createdAt' }) {
|
|
157
|
+
const db = ensureDbConnection();
|
|
158
|
+
const collectionName = normalizeCollectionName(collection);
|
|
159
|
+
const safeDateField = normalizeDateField(dateField);
|
|
160
|
+
const days = normalizeOlderThanDays(olderThanDays);
|
|
161
|
+
|
|
162
|
+
await ensureCollectionExists(db, collectionName);
|
|
163
|
+
|
|
164
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
165
|
+
const query = buildOlderThanQuery({ dateField: safeDateField, cutoff });
|
|
166
|
+
const mongoCollection = db.collection(collectionName);
|
|
167
|
+
|
|
168
|
+
const [candidateCount, collectionStats] = await Promise.all([
|
|
169
|
+
mongoCollection.countDocuments(query),
|
|
170
|
+
getCollectionStatsByName(db, collectionName),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const estimatedReclaimableBytes = estimateReclaimableBytes({
|
|
174
|
+
candidateCount,
|
|
175
|
+
collectionStats,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
collection: collectionName,
|
|
180
|
+
dateField: safeDateField,
|
|
181
|
+
olderThanDays: days,
|
|
182
|
+
cutoffIso: cutoff.toISOString(),
|
|
183
|
+
candidateCount,
|
|
184
|
+
estimatedReclaimableBytes,
|
|
185
|
+
collectionStats,
|
|
186
|
+
notes: [
|
|
187
|
+
'Estimate is based on average object size and is not guaranteed to be reclaimed physically on disk immediately.',
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function executeCollectionCleanup({
|
|
193
|
+
collection,
|
|
194
|
+
olderThanDays,
|
|
195
|
+
dateField = 'createdAt',
|
|
196
|
+
limit,
|
|
197
|
+
confirm,
|
|
198
|
+
}) {
|
|
199
|
+
if (confirm !== true) {
|
|
200
|
+
throw Object.assign(new Error('Cleanup confirmation is required (confirm=true)'), { code: 'VALIDATION' });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const db = ensureDbConnection();
|
|
204
|
+
const collectionName = normalizeCollectionName(collection);
|
|
205
|
+
const safeDateField = normalizeDateField(dateField);
|
|
206
|
+
const days = normalizeOlderThanDays(olderThanDays);
|
|
207
|
+
const maxDelete = normalizeLimit(limit, 5000);
|
|
208
|
+
|
|
209
|
+
await ensureCollectionExists(db, collectionName);
|
|
210
|
+
|
|
211
|
+
const start = Date.now();
|
|
212
|
+
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
213
|
+
const query = buildOlderThanQuery({ dateField: safeDateField, cutoff });
|
|
214
|
+
const mongoCollection = db.collection(collectionName);
|
|
215
|
+
|
|
216
|
+
const dryRun = await dryRunCollectionCleanup({
|
|
217
|
+
collection: collectionName,
|
|
218
|
+
olderThanDays: days,
|
|
219
|
+
dateField: safeDateField,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
let remaining = maxDelete;
|
|
223
|
+
let deletedCount = 0;
|
|
224
|
+
const batchSize = 1000;
|
|
225
|
+
|
|
226
|
+
while (remaining > 0) {
|
|
227
|
+
const currentBatch = Math.min(batchSize, remaining);
|
|
228
|
+
const ids = await mongoCollection
|
|
229
|
+
.find(query, { projection: { _id: 1 } })
|
|
230
|
+
.sort({ _id: 1 })
|
|
231
|
+
.limit(currentBatch)
|
|
232
|
+
.toArray();
|
|
233
|
+
|
|
234
|
+
if (!ids.length) break;
|
|
235
|
+
|
|
236
|
+
const idList = ids.map((d) => d._id).filter(Boolean);
|
|
237
|
+
if (!idList.length) break;
|
|
238
|
+
|
|
239
|
+
const out = await mongoCollection.deleteMany({ _id: { $in: idList } });
|
|
240
|
+
const deleted = Number(out?.deletedCount || 0);
|
|
241
|
+
deletedCount += deleted;
|
|
242
|
+
remaining -= idList.length;
|
|
243
|
+
|
|
244
|
+
if (deleted === 0) break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
collection: collectionName,
|
|
249
|
+
dateField: safeDateField,
|
|
250
|
+
olderThanDays: days,
|
|
251
|
+
cutoffIso: cutoff.toISOString(),
|
|
252
|
+
limitApplied: maxDelete,
|
|
253
|
+
dryRunCandidateCount: dryRun.candidateCount,
|
|
254
|
+
deletedCount,
|
|
255
|
+
estimatedReclaimableBytes: dryRun.estimatedReclaimableBytes,
|
|
256
|
+
durationMs: Date.now() - start,
|
|
257
|
+
notes: dryRun.notes,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function inferCollectionFields(collectionName, sampleSize = 10) {
|
|
262
|
+
const db = ensureDbConnection();
|
|
263
|
+
const coll = db.collection(normalizeCollectionName(collectionName));
|
|
264
|
+
|
|
265
|
+
const docs = await coll.find({}).limit(sampleSize).toArray();
|
|
266
|
+
const fieldSet = new Set();
|
|
267
|
+
|
|
268
|
+
for (const doc of docs) {
|
|
269
|
+
const keys = Object.keys(doc);
|
|
270
|
+
for (const key of keys) {
|
|
271
|
+
fieldSet.add(key);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Array.from(fieldSet).sort();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
toSafeJsonError,
|
|
280
|
+
getMongoGlobalStats,
|
|
281
|
+
listCollectionStats,
|
|
282
|
+
getOverviewStats,
|
|
283
|
+
dryRunCollectionCleanup,
|
|
284
|
+
executeCollectionCleanup,
|
|
285
|
+
inferCollectionFields,
|
|
286
|
+
};
|
|
@@ -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) {
|
|
@@ -110,6 +122,108 @@ function clearAllJsonConfigCache() {
|
|
|
110
122
|
cache.clear();
|
|
111
123
|
}
|
|
112
124
|
|
|
125
|
+
// Enhanced cache helpers
|
|
126
|
+
function isJsonConfigCached(slug) {
|
|
127
|
+
const key = String(slug || '').trim();
|
|
128
|
+
if (!key) return false;
|
|
129
|
+
const entry = cache.get(key);
|
|
130
|
+
if (!entry) return false;
|
|
131
|
+
if (typeof entry.expiresAt === 'number' && Date.now() > entry.expiresAt) {
|
|
132
|
+
cache.delete(key);
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getJsonConfigCacheInfo(slug) {
|
|
139
|
+
const key = String(slug || '').trim();
|
|
140
|
+
if (!key) return null;
|
|
141
|
+
const entry = cache.get(key);
|
|
142
|
+
if (!entry) return { exists: false };
|
|
143
|
+
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
const isExpired = typeof entry.expiresAt === 'number' && now > entry.expiresAt;
|
|
146
|
+
if (isExpired) {
|
|
147
|
+
cache.delete(key);
|
|
148
|
+
return { exists: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
exists: true,
|
|
153
|
+
expiresAt: entry.expiresAt,
|
|
154
|
+
ttlRemaining: entry.expiresAt ? Math.max(0, Math.floor((entry.expiresAt - now) / 1000)) : null,
|
|
155
|
+
size: JSON.stringify(entry.value).length
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function clearJsonConfigCacheIfExists(slug) {
|
|
160
|
+
const key = String(slug || '').trim();
|
|
161
|
+
if (!key) return false;
|
|
162
|
+
const existed = cache.has(key);
|
|
163
|
+
cache.delete(key);
|
|
164
|
+
return existed;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function clearJsonConfigCacheIfExpired(slug) {
|
|
168
|
+
const info = getJsonConfigCacheInfo(slug);
|
|
169
|
+
if (!info || !info.exists) return false;
|
|
170
|
+
if (info.ttlRemaining !== null && info.ttlRemaining <= 0) {
|
|
171
|
+
cache.delete(String(slug));
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function clearJsonConfigCacheBatch(slugs) {
|
|
178
|
+
if (!Array.isArray(slugs)) return 0;
|
|
179
|
+
let cleared = 0;
|
|
180
|
+
slugs.forEach(slug => {
|
|
181
|
+
if (clearJsonConfigCacheIfExists(slug)) cleared++;
|
|
182
|
+
});
|
|
183
|
+
return cleared;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clearJsonConfigCacheByPattern(pattern) {
|
|
187
|
+
if (!pattern || typeof pattern !== 'string') return 0;
|
|
188
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
189
|
+
let cleared = 0;
|
|
190
|
+
for (const key of cache.keys()) {
|
|
191
|
+
if (regex.test(key)) {
|
|
192
|
+
cache.delete(key);
|
|
193
|
+
cleared++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return cleared;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getJsonConfigCacheStats() {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
let entries = 0;
|
|
202
|
+
let expired = 0;
|
|
203
|
+
let totalSize = 0;
|
|
204
|
+
|
|
205
|
+
for (const [key, entry] of cache.entries()) {
|
|
206
|
+
entries++;
|
|
207
|
+
if (typeof entry.expiresAt === 'number' && now > entry.expiresAt) {
|
|
208
|
+
expired++;
|
|
209
|
+
} else {
|
|
210
|
+
totalSize += JSON.stringify(entry.value).length;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
totalEntries: entries,
|
|
216
|
+
expiredEntries: expired,
|
|
217
|
+
activeEntries: entries - expired,
|
|
218
|
+
totalSizeBytes: totalSize,
|
|
219
|
+
keys: Array.from(cache.keys())
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getJsonConfigCacheKeys() {
|
|
224
|
+
return Array.from(cache.keys());
|
|
225
|
+
}
|
|
226
|
+
|
|
113
227
|
async function listJsonConfigs() {
|
|
114
228
|
return JsonConfig.find()
|
|
115
229
|
.sort({ updatedAt: -1 })
|
|
@@ -122,7 +236,7 @@ async function getJsonConfigById(id) {
|
|
|
122
236
|
}
|
|
123
237
|
|
|
124
238
|
async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTtlSeconds = 0, alias }) {
|
|
125
|
-
|
|
239
|
+
logger.log('createJsonConfig called with:', { title, jsonRaw, publicEnabled, cacheTtlSeconds, alias });
|
|
126
240
|
|
|
127
241
|
const normalizedTitle = String(title || '').trim();
|
|
128
242
|
if (!normalizedTitle) {
|
|
@@ -142,7 +256,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
|
|
|
142
256
|
let normalizedAlias = null;
|
|
143
257
|
if (alias !== undefined && alias !== null) {
|
|
144
258
|
normalizedAlias = normalizeAlias(alias);
|
|
145
|
-
|
|
259
|
+
logger.log('Normalized alias:', normalizedAlias);
|
|
146
260
|
if (normalizedAlias && !(await validateAliasUniqueness(normalizedAlias))) {
|
|
147
261
|
const err = new Error('Alias must be unique and not conflict with existing slugs or aliases');
|
|
148
262
|
err.code = 'ALIAS_NOT_UNIQUE';
|
|
@@ -175,7 +289,7 @@ async function createJsonConfig({ title, jsonRaw, publicEnabled = false, cacheTt
|
|
|
175
289
|
}
|
|
176
290
|
|
|
177
291
|
async function updateJsonConfig(id, patch) {
|
|
178
|
-
|
|
292
|
+
logger.log('updateJsonConfig called with id:', id, 'patch:', patch);
|
|
179
293
|
|
|
180
294
|
const doc = await JsonConfig.findById(id);
|
|
181
295
|
if (!doc) {
|
|
@@ -184,7 +298,7 @@ async function updateJsonConfig(id, patch) {
|
|
|
184
298
|
throw err;
|
|
185
299
|
}
|
|
186
300
|
|
|
187
|
-
|
|
301
|
+
logger.log('Found document:', doc.toObject());
|
|
188
302
|
|
|
189
303
|
const oldSlug = doc.slug;
|
|
190
304
|
const oldAlias = doc.alias;
|
|
@@ -222,14 +336,14 @@ async function updateJsonConfig(id, patch) {
|
|
|
222
336
|
|
|
223
337
|
if (patch && Object.prototype.hasOwnProperty.call(patch, 'alias')) {
|
|
224
338
|
const newAlias = patch.alias;
|
|
225
|
-
|
|
339
|
+
logger.log('Processing alias update. newAlias:', newAlias);
|
|
226
340
|
|
|
227
341
|
if (newAlias === null || newAlias === undefined || newAlias === '') {
|
|
228
342
|
doc.alias = undefined;
|
|
229
|
-
|
|
343
|
+
logger.log('Setting alias to undefined');
|
|
230
344
|
} else {
|
|
231
345
|
const normalizedAlias = normalizeAlias(newAlias);
|
|
232
|
-
|
|
346
|
+
logger.log('Normalized alias for update:', normalizedAlias);
|
|
233
347
|
|
|
234
348
|
if (!normalizedAlias) {
|
|
235
349
|
const err = new Error('Invalid alias format');
|
|
@@ -244,7 +358,7 @@ async function updateJsonConfig(id, patch) {
|
|
|
244
358
|
}
|
|
245
359
|
|
|
246
360
|
doc.alias = normalizedAlias;
|
|
247
|
-
|
|
361
|
+
logger.log('Setting alias to:', normalizedAlias);
|
|
248
362
|
}
|
|
249
363
|
}
|
|
250
364
|
|
|
@@ -252,9 +366,9 @@ async function updateJsonConfig(id, patch) {
|
|
|
252
366
|
doc.slug = await generateUniqueSlugFromTitle(doc.title);
|
|
253
367
|
}
|
|
254
368
|
|
|
255
|
-
|
|
369
|
+
logger.log('Document before save:', doc.toObject());
|
|
256
370
|
await doc.save();
|
|
257
|
-
|
|
371
|
+
logger.log('Document after save:', doc.toObject());
|
|
258
372
|
|
|
259
373
|
clearJsonConfigCache(oldSlug);
|
|
260
374
|
clearJsonConfigCache(doc.slug);
|
|
@@ -374,16 +488,176 @@ async function getJsonConfigPublicPayload(slug, { raw = false } = {}) {
|
|
|
374
488
|
};
|
|
375
489
|
}
|
|
376
490
|
|
|
491
|
+
// Cache-aware update helpers
|
|
492
|
+
async function updateJsonConfigWithCacheInvalidation(id, patch) {
|
|
493
|
+
logger.log('updateJsonConfigWithCacheInvalidation called with id:', id, 'patch:', patch);
|
|
494
|
+
|
|
495
|
+
const doc = await JsonConfig.findById(id);
|
|
496
|
+
if (!doc) {
|
|
497
|
+
const err = new Error('JSON config not found');
|
|
498
|
+
err.code = 'NOT_FOUND';
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const oldSlug = doc.slug;
|
|
503
|
+
const oldAlias = doc.alias;
|
|
504
|
+
|
|
505
|
+
// Apply updates using existing update logic
|
|
506
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'title')) {
|
|
507
|
+
const title = String(patch.title || '').trim();
|
|
508
|
+
if (!title) {
|
|
509
|
+
const err = new Error('title is required');
|
|
510
|
+
err.code = 'VALIDATION';
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
doc.title = title;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'publicEnabled')) {
|
|
517
|
+
doc.publicEnabled = Boolean(patch.publicEnabled);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'cacheTtlSeconds')) {
|
|
521
|
+
const ttl = Number(patch.cacheTtlSeconds || 0);
|
|
522
|
+
doc.cacheTtlSeconds = Number.isNaN(ttl) ? 0 : Math.max(0, ttl);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'jsonRaw')) {
|
|
526
|
+
if (patch.jsonRaw === null || patch.jsonRaw === undefined) {
|
|
527
|
+
const err = new Error('jsonRaw is required');
|
|
528
|
+
err.code = 'VALIDATION';
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
parseJsonOrThrow(patch.jsonRaw);
|
|
533
|
+
doc.jsonRaw = String(patch.jsonRaw);
|
|
534
|
+
doc.jsonHash = computeJsonHash(doc.jsonRaw);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (patch && Object.prototype.hasOwnProperty.call(patch, 'alias')) {
|
|
538
|
+
const newAlias = patch.alias;
|
|
539
|
+
|
|
540
|
+
if (newAlias === null || newAlias === undefined || newAlias === '') {
|
|
541
|
+
doc.alias = undefined;
|
|
542
|
+
} else {
|
|
543
|
+
const normalizedAlias = normalizeAlias(newAlias);
|
|
544
|
+
|
|
545
|
+
if (!normalizedAlias) {
|
|
546
|
+
const err = new Error('Invalid alias format');
|
|
547
|
+
err.code = 'VALIDATION';
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (!(await validateAliasUniqueness(normalizedAlias, doc._id))) {
|
|
552
|
+
const err = new Error('Alias must be unique and not conflict with existing slugs or aliases');
|
|
553
|
+
err.code = 'ALIAS_NOT_UNIQUE';
|
|
554
|
+
throw err;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
doc.alias = normalizedAlias;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
await doc.save();
|
|
562
|
+
|
|
563
|
+
// Clear all related cache entries
|
|
564
|
+
clearJsonConfigCache(oldSlug);
|
|
565
|
+
clearJsonConfigCache(doc.slug);
|
|
566
|
+
if (oldAlias) {
|
|
567
|
+
clearJsonConfigCache(oldAlias);
|
|
568
|
+
}
|
|
569
|
+
if (doc.alias) {
|
|
570
|
+
clearJsonConfigCache(doc.alias);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return doc.toObject();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function updateJsonConfigValueBySlug(slug, updateFn, options = {}) {
|
|
577
|
+
const key = String(slug || '').trim();
|
|
578
|
+
if (!key) {
|
|
579
|
+
const err = new Error('slug is required');
|
|
580
|
+
err.code = 'VALIDATION';
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (typeof updateFn !== 'function') {
|
|
585
|
+
const err = new Error('updateFn must be a function');
|
|
586
|
+
err.code = 'VALIDATION';
|
|
587
|
+
throw err;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Get current value (bypass cache to ensure fresh data)
|
|
591
|
+
const currentValue = await getJsonConfigValueBySlug(key, { bypassCache: true });
|
|
592
|
+
|
|
593
|
+
// Apply update function
|
|
594
|
+
let newValue;
|
|
595
|
+
try {
|
|
596
|
+
newValue = await updateFn(currentValue);
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const err = new Error(`Update function failed: ${error.message}`);
|
|
599
|
+
err.code = 'UPDATE_FAILED';
|
|
600
|
+
err.cause = error;
|
|
601
|
+
throw err;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Validate new value
|
|
605
|
+
const jsonRaw = JSON.stringify(newValue);
|
|
606
|
+
parseJsonOrThrow(jsonRaw); // This will throw if invalid JSON
|
|
607
|
+
|
|
608
|
+
// Find the document
|
|
609
|
+
const doc = await JsonConfig.findOne({
|
|
610
|
+
$or: [
|
|
611
|
+
{ slug: key },
|
|
612
|
+
{ alias: key }
|
|
613
|
+
]
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (!doc) {
|
|
617
|
+
const err = new Error('JSON config not found');
|
|
618
|
+
err.code = 'NOT_FOUND';
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Update the document
|
|
623
|
+
doc.jsonRaw = jsonRaw;
|
|
624
|
+
doc.jsonHash = computeJsonHash(jsonRaw);
|
|
625
|
+
await doc.save();
|
|
626
|
+
|
|
627
|
+
// Clear cache if requested (default: true)
|
|
628
|
+
if (options.invalidateCache !== false) {
|
|
629
|
+
clearJsonConfigCache(doc.slug);
|
|
630
|
+
if (doc.alias) {
|
|
631
|
+
clearJsonConfigCache(doc.alias);
|
|
632
|
+
}
|
|
633
|
+
clearJsonConfigCache(key);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return newValue;
|
|
637
|
+
}
|
|
638
|
+
|
|
377
639
|
module.exports = {
|
|
378
640
|
normalizeSlugBase,
|
|
379
641
|
generateUniqueSlugFromTitle,
|
|
380
642
|
parseJsonOrThrow,
|
|
381
643
|
clearJsonConfigCache,
|
|
382
644
|
clearAllJsonConfigCache,
|
|
645
|
+
// Enhanced cache helpers
|
|
646
|
+
isJsonConfigCached,
|
|
647
|
+
getJsonConfigCacheInfo,
|
|
648
|
+
clearJsonConfigCacheIfExists,
|
|
649
|
+
clearJsonConfigCacheIfExpired,
|
|
650
|
+
clearJsonConfigCacheBatch,
|
|
651
|
+
clearJsonConfigCacheByPattern,
|
|
652
|
+
getJsonConfigCacheStats,
|
|
653
|
+
getJsonConfigCacheKeys,
|
|
654
|
+
// Core functions
|
|
383
655
|
listJsonConfigs,
|
|
384
656
|
getJsonConfigById,
|
|
385
657
|
createJsonConfig,
|
|
386
658
|
updateJsonConfig,
|
|
659
|
+
updateJsonConfigWithCacheInvalidation,
|
|
660
|
+
updateJsonConfigValueBySlug,
|
|
387
661
|
regenerateJsonConfigSlug,
|
|
388
662
|
deleteJsonConfig,
|
|
389
663
|
getJsonConfig: getJsonConfigValueBySlug,
|