@intranefr/superbackend 1.5.3 → 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 +7 -0
- package/package.json +3 -1
- 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 +25 -5
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +0 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +454 -153
- package/src/models/GlobalSetting.js +11 -1
- 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 +2 -2
- 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 -8
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +3 -9
- 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 +2 -2
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +2 -2
- 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/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/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +262 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -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-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +143 -2
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-login.ejs +286 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +2 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
|
@@ -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
|
+
};
|
|
@@ -122,6 +122,108 @@ function clearAllJsonConfigCache() {
|
|
|
122
122
|
cache.clear();
|
|
123
123
|
}
|
|
124
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
|
+
|
|
125
227
|
async function listJsonConfigs() {
|
|
126
228
|
return JsonConfig.find()
|
|
127
229
|
.sort({ updatedAt: -1 })
|
|
@@ -386,16 +488,176 @@ async function getJsonConfigPublicPayload(slug, { raw = false } = {}) {
|
|
|
386
488
|
};
|
|
387
489
|
}
|
|
388
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
|
+
|
|
389
639
|
module.exports = {
|
|
390
640
|
normalizeSlugBase,
|
|
391
641
|
generateUniqueSlugFromTitle,
|
|
392
642
|
parseJsonOrThrow,
|
|
393
643
|
clearJsonConfigCache,
|
|
394
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
|
|
395
655
|
listJsonConfigs,
|
|
396
656
|
getJsonConfigById,
|
|
397
657
|
createJsonConfig,
|
|
398
658
|
updateJsonConfig,
|
|
659
|
+
updateJsonConfigWithCacheInvalidation,
|
|
660
|
+
updateJsonConfigValueBySlug,
|
|
399
661
|
regenerateJsonConfigSlug,
|
|
400
662
|
deleteJsonConfig,
|
|
401
663
|
getJsonConfig: getJsonConfigValueBySlug,
|