@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.
Files changed (134) hide show
  1. package/cookies.txt +6 -0
  2. package/cookies1.txt +6 -0
  3. package/cookies2.txt +6 -0
  4. package/cookies3.txt +6 -0
  5. package/cookies4.txt +5 -0
  6. package/cookies_old.txt +5 -0
  7. package/cookies_old_test.txt +6 -0
  8. package/cookies_super.txt +5 -0
  9. package/cookies_super_test.txt +6 -0
  10. package/cookies_test.txt +6 -0
  11. package/index.js +9 -0
  12. package/manage.js +745 -0
  13. package/package.json +6 -2
  14. package/plugins/core-waiting-list-migration/README.md +118 -0
  15. package/plugins/core-waiting-list-migration/index.js +438 -0
  16. package/plugins/global-settings-presets/index.js +20 -0
  17. package/plugins/hello-cli/index.js +17 -0
  18. package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
  19. package/plugins/ui-components-seeder/components/suiToast.js +186 -0
  20. package/plugins/ui-components-seeder/index.js +31 -0
  21. package/public/js/admin-ui-components-preview.js +281 -0
  22. package/public/js/admin-ui-components.js +408 -0
  23. package/public/js/llm-provider-model-picker.js +193 -0
  24. package/public/test-iframe-fix.html +63 -0
  25. package/public/test-iframe.html +14 -0
  26. package/src/admin/endpointRegistry.js +68 -0
  27. package/src/controllers/admin.controller.js +36 -10
  28. package/src/controllers/adminAgents.controller.js +37 -0
  29. package/src/controllers/adminDataCleanup.controller.js +45 -0
  30. package/src/controllers/adminLlm.controller.js +19 -8
  31. package/src/controllers/adminLogin.controller.js +269 -0
  32. package/src/controllers/adminMarkdowns.controller.js +157 -0
  33. package/src/controllers/adminPlugins.controller.js +55 -0
  34. package/src/controllers/adminRegistry.controller.js +106 -0
  35. package/src/controllers/adminScripts.controller.js +138 -0
  36. package/src/controllers/adminStats.controller.js +4 -4
  37. package/src/controllers/adminTelegram.controller.js +72 -0
  38. package/src/controllers/markdowns.controller.js +42 -0
  39. package/src/controllers/registry.controller.js +32 -0
  40. package/src/controllers/waitingList.controller.js +52 -74
  41. package/src/helpers/mongooseHelper.js +6 -6
  42. package/src/helpers/scriptBase.js +2 -2
  43. package/src/middleware/auth.js +71 -1
  44. package/src/middleware/rbac.js +62 -0
  45. package/src/middleware.js +584 -176
  46. package/src/models/Agent.js +105 -0
  47. package/src/models/AgentMessage.js +82 -0
  48. package/src/models/GlobalSetting.js +11 -1
  49. package/src/models/Markdown.js +75 -0
  50. package/src/models/ScriptRun.js +8 -0
  51. package/src/models/TelegramBot.js +42 -0
  52. package/src/models/UiComponent.js +2 -0
  53. package/src/models/User.js +1 -1
  54. package/src/routes/admin.routes.js +3 -3
  55. package/src/routes/adminAgents.routes.js +13 -0
  56. package/src/routes/adminAssets.routes.js +11 -11
  57. package/src/routes/adminBlog.routes.js +2 -2
  58. package/src/routes/adminBlogAi.routes.js +2 -2
  59. package/src/routes/adminBlogAutomation.routes.js +2 -2
  60. package/src/routes/adminCache.routes.js +2 -2
  61. package/src/routes/adminConsoleManager.routes.js +2 -2
  62. package/src/routes/adminCrons.routes.js +2 -2
  63. package/src/routes/adminDataCleanup.routes.js +26 -0
  64. package/src/routes/adminDbBrowser.routes.js +2 -2
  65. package/src/routes/adminEjsVirtual.routes.js +2 -2
  66. package/src/routes/adminFeatureFlags.routes.js +6 -6
  67. package/src/routes/adminHeadless.routes.js +2 -2
  68. package/src/routes/adminHealthChecks.routes.js +2 -2
  69. package/src/routes/adminI18n.routes.js +2 -2
  70. package/src/routes/adminJsonConfigs.routes.js +8 -8
  71. package/src/routes/adminLlm.routes.js +8 -7
  72. package/src/routes/adminLogin.routes.js +23 -0
  73. package/src/routes/adminMarkdowns.routes.js +10 -0
  74. package/src/routes/adminMigration.routes.js +12 -12
  75. package/src/routes/adminPages.routes.js +2 -2
  76. package/src/routes/adminPlugins.routes.js +15 -0
  77. package/src/routes/adminProxy.routes.js +2 -2
  78. package/src/routes/adminRateLimits.routes.js +8 -8
  79. package/src/routes/adminRbac.routes.js +2 -2
  80. package/src/routes/adminRegistry.routes.js +24 -0
  81. package/src/routes/adminScripts.routes.js +6 -3
  82. package/src/routes/adminSeoConfig.routes.js +10 -10
  83. package/src/routes/adminTelegram.routes.js +14 -0
  84. package/src/routes/adminTerminals.routes.js +2 -2
  85. package/src/routes/adminUiComponents.routes.js +2 -2
  86. package/src/routes/adminUploadNamespaces.routes.js +7 -7
  87. package/src/routes/blogInternal.routes.js +2 -2
  88. package/src/routes/experiments.routes.js +2 -2
  89. package/src/routes/formsAdmin.routes.js +6 -6
  90. package/src/routes/globalSettings.routes.js +8 -8
  91. package/src/routes/internalExperiments.routes.js +2 -2
  92. package/src/routes/markdowns.routes.js +16 -0
  93. package/src/routes/notificationAdmin.routes.js +7 -7
  94. package/src/routes/orgAdmin.routes.js +16 -16
  95. package/src/routes/pages.routes.js +3 -3
  96. package/src/routes/registry.routes.js +11 -0
  97. package/src/routes/stripeAdmin.routes.js +12 -12
  98. package/src/routes/userAdmin.routes.js +7 -7
  99. package/src/routes/waitingListAdmin.routes.js +2 -2
  100. package/src/routes/workflows.routes.js +3 -3
  101. package/src/services/agent.service.js +546 -0
  102. package/src/services/agentHistory.service.js +345 -0
  103. package/src/services/agentTools.service.js +578 -0
  104. package/src/services/dataCleanup.service.js +286 -0
  105. package/src/services/jsonConfigs.service.js +284 -10
  106. package/src/services/llm.service.js +219 -6
  107. package/src/services/markdowns.service.js +522 -0
  108. package/src/services/plugins.service.js +348 -0
  109. package/src/services/registry.service.js +452 -0
  110. package/src/services/scriptsRunner.service.js +328 -37
  111. package/src/services/telegram.service.js +130 -0
  112. package/src/services/uiComponents.service.js +180 -0
  113. package/src/services/waitingListJson.service.js +401 -0
  114. package/src/utils/rbac/rightsRegistry.js +118 -0
  115. package/test-access.js +63 -0
  116. package/test-iframe-fix.html +63 -0
  117. package/test-iframe.html +14 -0
  118. package/views/admin-403.ejs +92 -0
  119. package/views/admin-agents.ejs +273 -0
  120. package/views/admin-coolify-deploy.ejs +8 -8
  121. package/views/admin-dashboard-home.ejs +52 -2
  122. package/views/admin-dashboard.ejs +179 -7
  123. package/views/admin-data-cleanup.ejs +357 -0
  124. package/views/admin-experiments.ejs +1 -1
  125. package/views/admin-login.ejs +286 -0
  126. package/views/admin-markdowns.ejs +905 -0
  127. package/views/admin-plugins-system.ejs +223 -0
  128. package/views/admin-scripts.ejs +221 -4
  129. package/views/admin-telegram.ejs +269 -0
  130. package/views/admin-ui-components.ejs +82 -402
  131. package/views/admin-users.ejs +207 -11
  132. package/views/partials/dashboard/nav-items.ejs +5 -0
  133. package/views/partials/llm-provider-model-picker.ejs +0 -161
  134. 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
- console.log('createJsonConfig called with:', { title, jsonRaw, publicEnabled, cacheTtlSeconds, alias });
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
- console.log('Normalized alias:', normalizedAlias);
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
- console.log('updateJsonConfig called with id:', id, 'patch:', patch);
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
- console.log('Found document:', doc.toObject());
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
- console.log('Processing alias update. newAlias:', newAlias);
339
+ logger.log('Processing alias update. newAlias:', newAlias);
226
340
 
227
341
  if (newAlias === null || newAlias === undefined || newAlias === '') {
228
342
  doc.alias = undefined;
229
- console.log('Setting alias to undefined');
343
+ logger.log('Setting alias to undefined');
230
344
  } else {
231
345
  const normalizedAlias = normalizeAlias(newAlias);
232
- console.log('Normalized alias for update:', normalizedAlias);
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
- console.log('Setting alias to:', normalizedAlias);
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
- console.log('Document before save:', doc.toObject());
369
+ logger.log('Document before save:', doc.toObject());
256
370
  await doc.save();
257
- console.log('Document after save:', doc.toObject());
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,