@intranefr/superbackend 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +185 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +738 -0
  120. package/src/services/consoleOverride.service.js +7 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,342 @@
1
+ const GlobalSetting = require('../models/GlobalSetting');
2
+ const cacheLayer = require('../services/cacheLayer.service');
3
+ const globalSettingsService = require('../services/globalSettings.service');
4
+ const { encryptString } = require('../utils/encryption');
5
+ const { logAuditSync } = require('../services/auditLogger');
6
+
7
+ function toSafeJsonError(error) {
8
+ const msg = error?.message || 'Operation failed';
9
+ const code = error?.code;
10
+ if (code === 'VALIDATION') return { status: 400, body: { error: msg } };
11
+ if (code === 'NOT_FOUND') return { status: 404, body: { error: msg } };
12
+ if (code === 'CONFLICT') return { status: 409, body: { error: msg } };
13
+ return { status: 500, body: { error: msg } };
14
+ }
15
+
16
+ function audit(req, event) {
17
+ logAuditSync({
18
+ req,
19
+ action: event.action,
20
+ outcome: event.outcome,
21
+ entityType: event.entityType || 'CacheLayer',
22
+ entityId: event.entityId || null,
23
+ targetType: event.targetType || 'CacheLayer',
24
+ targetId: event.targetId || null,
25
+ before: event.before || null,
26
+ after: event.after || null,
27
+ details: event.details || undefined,
28
+ });
29
+ }
30
+
31
+ function normalizeNamespace(ns) {
32
+ const v = String(ns || '').trim();
33
+ return v || 'default';
34
+ }
35
+
36
+ function normalizeKey(key) {
37
+ const v = String(key || '').trim();
38
+ if (!v) throw Object.assign(new Error('key is required'), { code: 'VALIDATION' });
39
+ return v;
40
+ }
41
+
42
+ async function upsertSetting({ key, type, description, value, public: isPublic = false }) {
43
+ const existing = await GlobalSetting.findOne({ key });
44
+ if (!existing) {
45
+ const storedValue = type === 'encrypted' ? JSON.stringify(encryptString(String(value || ''))) : String(value ?? '');
46
+ await GlobalSetting.create({
47
+ key,
48
+ type,
49
+ description,
50
+ value: storedValue,
51
+ templateVariables: [],
52
+ public: Boolean(isPublic),
53
+ });
54
+ return;
55
+ }
56
+
57
+ if (existing.type !== type) {
58
+ existing.type = type;
59
+ }
60
+
61
+ if (existing.description !== description) {
62
+ existing.description = description;
63
+ }
64
+
65
+ if (type === 'encrypted') {
66
+ existing.value = JSON.stringify(encryptString(String(value || '')));
67
+ } else {
68
+ existing.value = String(value ?? '');
69
+ }
70
+
71
+ if (existing.public !== Boolean(isPublic)) {
72
+ existing.public = Boolean(isPublic);
73
+ }
74
+
75
+ await existing.save();
76
+ }
77
+
78
+ exports.getConfig = async (req, res) => {
79
+ try {
80
+ const cfg = await cacheLayer.getConfig();
81
+
82
+ res.json({
83
+ config: {
84
+ backend: cfg.backend,
85
+ evictionPolicy: cfg.evictionPolicy,
86
+ redisPrefix: cfg.redisPrefix,
87
+ redisUrlConfigured: Boolean(cfg.redisUrl),
88
+ offloadThresholdBytes: cfg.offloadThresholdBytes,
89
+ maxEntryBytes: cfg.maxEntryBytes,
90
+ defaultTtlSeconds: cfg.defaultTtlSeconds,
91
+ atRestFormat: cfg.atRestFormat,
92
+ },
93
+ });
94
+ } catch (err) {
95
+ const safe = toSafeJsonError(err);
96
+ res.status(safe.status).json(safe.body);
97
+ }
98
+ };
99
+
100
+ exports.updateConfig = async (req, res) => {
101
+ try {
102
+ const payload = req.body || {};
103
+
104
+ const backend = String(payload.backend || 'memory').toLowerCase();
105
+ const evictionPolicy = String(payload.evictionPolicy || 'lru').toLowerCase();
106
+ const redisUrl = payload.redisUrl;
107
+ const redisPrefix = payload.redisPrefix;
108
+ const offloadThresholdBytes = payload.offloadThresholdBytes;
109
+ const maxEntryBytes = payload.maxEntryBytes;
110
+ const defaultTtlSeconds = payload.defaultTtlSeconds;
111
+ const atRestFormat = String(payload.atRestFormat || 'string').toLowerCase();
112
+
113
+ const before = await cacheLayer.getConfig();
114
+
115
+ await upsertSetting({
116
+ key: 'CACHE_LAYER_BACKEND',
117
+ type: 'string',
118
+ description: 'Cache layer primary backend (memory or redis).',
119
+ value: backend === 'redis' ? 'redis' : 'memory',
120
+ public: false,
121
+ });
122
+
123
+ await upsertSetting({
124
+ key: 'CACHE_LAYER_EVICTION_POLICY',
125
+ type: 'string',
126
+ description: 'Cache layer eviction policy for memory backend (fifo, lru, lfu).',
127
+ value: ['fifo', 'lru', 'lfu'].includes(evictionPolicy) ? evictionPolicy : 'lru',
128
+ public: false,
129
+ });
130
+
131
+ if (redisPrefix !== undefined) {
132
+ await upsertSetting({
133
+ key: 'CACHE_LAYER_REDIS_PREFIX',
134
+ type: 'string',
135
+ description: 'Redis key prefix for Cache Layer.',
136
+ value: String(redisPrefix || 'superbackend:'),
137
+ public: false,
138
+ });
139
+ }
140
+
141
+ if (redisUrl !== undefined) {
142
+ await upsertSetting({
143
+ key: 'CACHE_LAYER_REDIS_URL',
144
+ type: 'encrypted',
145
+ description: 'Redis URL for Cache Layer (encrypted).',
146
+ value: String(redisUrl || ''),
147
+ public: false,
148
+ });
149
+ }
150
+
151
+ if (offloadThresholdBytes !== undefined) {
152
+ await upsertSetting({
153
+ key: 'CACHE_LAYER_OFFLOAD_THRESHOLD_BYTES',
154
+ type: 'number',
155
+ description: 'In-memory cache offload threshold in bytes before spilling to Mongo.',
156
+ value: String(offloadThresholdBytes),
157
+ public: false,
158
+ });
159
+ }
160
+
161
+ if (maxEntryBytes !== undefined) {
162
+ await upsertSetting({
163
+ key: 'CACHE_LAYER_MAX_ENTRY_BYTES',
164
+ type: 'number',
165
+ description: 'Maximum entry size in bytes for cache values.',
166
+ value: String(maxEntryBytes),
167
+ public: false,
168
+ });
169
+ }
170
+
171
+ if (defaultTtlSeconds !== undefined) {
172
+ await upsertSetting({
173
+ key: 'CACHE_LAYER_DEFAULT_TTL_SECONDS',
174
+ type: 'number',
175
+ description: 'Default TTL in seconds for cache entries when not specified.',
176
+ value: String(defaultTtlSeconds),
177
+ public: false,
178
+ });
179
+ }
180
+
181
+ await upsertSetting({
182
+ key: 'CACHE_LAYER_AT_REST_FORMAT',
183
+ type: 'string',
184
+ description: 'Cache entry at-rest format (string or base64).',
185
+ value: atRestFormat === 'base64' ? 'base64' : 'string',
186
+ public: false,
187
+ });
188
+
189
+ globalSettingsService.clearSettingsCache();
190
+
191
+ const after = await cacheLayer.getConfig();
192
+
193
+ audit(req, {
194
+ action: 'cache.config.update',
195
+ outcome: 'success',
196
+ entityType: 'CacheLayer',
197
+ entityId: null,
198
+ before,
199
+ after,
200
+ });
201
+
202
+ res.json({ ok: true });
203
+ } catch (err) {
204
+ audit(req, {
205
+ action: 'cache.config.update',
206
+ outcome: 'failure',
207
+ details: { error: err?.message || 'Operation failed' },
208
+ });
209
+ const safe = toSafeJsonError(err);
210
+ res.status(safe.status).json(safe.body);
211
+ }
212
+ };
213
+
214
+ exports.listKeys = async (req, res) => {
215
+ try {
216
+ const namespace = req.query.namespace ? normalizeNamespace(req.query.namespace) : null;
217
+ const prefix = req.query.prefix ? String(req.query.prefix) : null;
218
+
219
+ const out = await cacheLayer.listKeys({ namespace, prefix });
220
+ res.json({ items: out });
221
+ } catch (err) {
222
+ const safe = toSafeJsonError(err);
223
+ res.status(safe.status).json(safe.body);
224
+ }
225
+ };
226
+
227
+ exports.getEntry = async (req, res) => {
228
+ try {
229
+ const namespace = normalizeNamespace(req.query.namespace);
230
+ const key = normalizeKey(req.query.key);
231
+
232
+ const entry = await cacheLayer.getEntry(key, { namespace });
233
+ if (!entry) return res.status(404).json({ error: 'Not found' });
234
+
235
+ res.json({ item: entry });
236
+ } catch (err) {
237
+ const safe = toSafeJsonError(err);
238
+ res.status(safe.status).json(safe.body);
239
+ }
240
+ };
241
+
242
+ exports.setEntry = async (req, res) => {
243
+ try {
244
+ const payload = req.body || {};
245
+ const namespace = normalizeNamespace(payload.namespace);
246
+ const key = normalizeKey(payload.key);
247
+ const ttlSeconds = payload.ttlSeconds === undefined ? undefined : payload.ttlSeconds;
248
+ const allowNoExpiry = payload.allowNoExpiry === undefined ? true : Boolean(payload.allowNoExpiry);
249
+ const atRestFormat = payload.atRestFormat;
250
+
251
+ const before = await cacheLayer.getEntry(key, { namespace }).catch(() => null);
252
+
253
+ await cacheLayer.set(key, payload.value, { namespace, ttlSeconds, allowNoExpiry, atRestFormat });
254
+
255
+ const after = await cacheLayer.getEntry(key, { namespace }).catch(() => null);
256
+
257
+ audit(req, {
258
+ action: 'cache.entry.set',
259
+ outcome: 'success',
260
+ targetType: 'CacheEntry',
261
+ targetId: `${namespace}:${key}`,
262
+ before,
263
+ after,
264
+ });
265
+
266
+ res.json({ ok: true });
267
+ } catch (err) {
268
+ audit(req, {
269
+ action: 'cache.entry.set',
270
+ outcome: 'failure',
271
+ details: { error: err?.message || 'Operation failed' },
272
+ });
273
+ const safe = toSafeJsonError(err);
274
+ res.status(safe.status).json(safe.body);
275
+ }
276
+ };
277
+
278
+ exports.deleteEntry = async (req, res) => {
279
+ try {
280
+ const namespace = normalizeNamespace(req.query.namespace);
281
+ const key = normalizeKey(req.query.key);
282
+
283
+ const before = await cacheLayer.getEntry(key, { namespace }).catch(() => null);
284
+ const result = await cacheLayer.delete(key, { namespace });
285
+
286
+ audit(req, {
287
+ action: 'cache.entry.delete',
288
+ outcome: 'success',
289
+ targetType: 'CacheEntry',
290
+ targetId: `${namespace}:${key}`,
291
+ before,
292
+ after: null,
293
+ });
294
+
295
+ res.json({ ok: true, deleted: Boolean(result.ok) });
296
+ } catch (err) {
297
+ audit(req, {
298
+ action: 'cache.entry.delete',
299
+ outcome: 'failure',
300
+ details: { error: err?.message || 'Operation failed' },
301
+ });
302
+ const safe = toSafeJsonError(err);
303
+ res.status(safe.status).json(safe.body);
304
+ }
305
+ };
306
+
307
+ exports.clearCache = async (req, res) => {
308
+ try {
309
+ const payload = req.body || {};
310
+ const backend = String(payload.backend || 'all');
311
+ const namespace = payload.namespace ? normalizeNamespace(payload.namespace) : null;
312
+ const prefix = payload.prefix ? String(payload.prefix) : null;
313
+
314
+ const result = await cacheLayer.clear({ backend, namespace, prefix });
315
+
316
+ audit(req, {
317
+ action: 'cache.clear',
318
+ outcome: 'success',
319
+ details: { backend, namespace, prefix, cleared: result.cleared },
320
+ });
321
+
322
+ res.json(result);
323
+ } catch (err) {
324
+ audit(req, {
325
+ action: 'cache.clear',
326
+ outcome: 'failure',
327
+ details: { error: err?.message || 'Operation failed' },
328
+ });
329
+ const safe = toSafeJsonError(err);
330
+ res.status(safe.status).json(safe.body);
331
+ }
332
+ };
333
+
334
+ exports.metrics = async (req, res) => {
335
+ try {
336
+ const metrics = await cacheLayer.metrics();
337
+ res.json({ metrics });
338
+ } catch (err) {
339
+ const safe = toSafeJsonError(err);
340
+ res.status(safe.status).json(safe.body);
341
+ }
342
+ };
@@ -0,0 +1,141 @@
1
+ const ContextBlockDefinition = require('../models/ContextBlockDefinition');
2
+
3
+ function parseBool(value, fallback) {
4
+ if (value === undefined) return fallback;
5
+ if (typeof value === 'boolean') return value;
6
+ const v = String(value).trim().toLowerCase();
7
+ if (v === 'true' || v === '1' || v === 'yes') return true;
8
+ if (v === 'false' || v === '0' || v === 'no') return false;
9
+ return fallback;
10
+ }
11
+
12
+ function normalizeCode(code) {
13
+ return String(code || '').trim().toLowerCase();
14
+ }
15
+
16
+ function normalizeProps(props) {
17
+ if (!props) return {};
18
+ if (typeof props !== 'object' || Array.isArray(props)) return null;
19
+ return props;
20
+ }
21
+
22
+ function normalizeType(type) {
23
+ return String(type || '').trim();
24
+ }
25
+
26
+ exports.list = async (req, res) => {
27
+ try {
28
+ const onlyActive = parseBool(req.query?.active, null);
29
+ const filter = {};
30
+ if (onlyActive !== null) filter.isActive = onlyActive;
31
+
32
+ const items = await ContextBlockDefinition.find(filter).sort({ updatedAt: -1 }).lean();
33
+ return res.json({ items });
34
+ } catch (error) {
35
+ console.error('[adminContextBlockDefinitions] list error:', error);
36
+ return res.status(500).json({ error: 'Failed to list context block definitions' });
37
+ }
38
+ };
39
+
40
+ exports.create = async (req, res) => {
41
+ try {
42
+ const code = normalizeCode(req.body?.code);
43
+ const label = String(req.body?.label || '').trim();
44
+ const type = normalizeType(req.body?.type);
45
+
46
+ if (!code) return res.status(400).json({ error: 'code is required' });
47
+ if (!label) return res.status(400).json({ error: 'label is required' });
48
+ if (!type) return res.status(400).json({ error: 'type is required' });
49
+
50
+ const props = normalizeProps(req.body?.props);
51
+ if (props === null) return res.status(400).json({ error: 'props must be an object' });
52
+
53
+ const doc = await ContextBlockDefinition.create({
54
+ code,
55
+ label,
56
+ description: String(req.body?.description || ''),
57
+ type,
58
+ props: props || {},
59
+ version: Number(req.body?.version || 1) || 1,
60
+ isActive: parseBool(req.body?.isActive, true),
61
+ });
62
+
63
+ return res.status(201).json({ item: doc.toObject() });
64
+ } catch (error) {
65
+ console.error('[adminContextBlockDefinitions] create error:', error);
66
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
67
+ if (error?.code === 11000) return res.status(409).json({ error: 'Context block definition already exists' });
68
+ return res.status(500).json({ error: 'Failed to create context block definition' });
69
+ }
70
+ };
71
+
72
+ exports.get = async (req, res) => {
73
+ try {
74
+ const code = normalizeCode(req.params?.code);
75
+ const item = await ContextBlockDefinition.findOne({ code }).lean();
76
+ if (!item) return res.status(404).json({ error: 'Context block definition not found' });
77
+ return res.json({ item });
78
+ } catch (error) {
79
+ console.error('[adminContextBlockDefinitions] get error:', error);
80
+ return res.status(500).json({ error: 'Failed to load context block definition' });
81
+ }
82
+ };
83
+
84
+ exports.update = async (req, res) => {
85
+ try {
86
+ const code = normalizeCode(req.params?.code);
87
+ const doc = await ContextBlockDefinition.findOne({ code });
88
+ if (!doc) return res.status(404).json({ error: 'Context block definition not found' });
89
+
90
+ if (req.body?.label !== undefined) {
91
+ const label = String(req.body.label || '').trim();
92
+ if (!label) return res.status(400).json({ error: 'label is required' });
93
+ doc.label = label;
94
+ }
95
+
96
+ if (req.body?.description !== undefined) doc.description = String(req.body.description || '');
97
+
98
+ if (req.body?.type !== undefined) {
99
+ const type = normalizeType(req.body.type);
100
+ if (!type) return res.status(400).json({ error: 'type is required' });
101
+ doc.type = type;
102
+ }
103
+
104
+ if (req.body?.props !== undefined) {
105
+ const props = normalizeProps(req.body.props);
106
+ if (props === null) return res.status(400).json({ error: 'props must be an object' });
107
+ doc.props = props;
108
+ }
109
+
110
+ if (req.body?.version !== undefined) {
111
+ const v = Number(req.body.version);
112
+ if (!Number.isFinite(v) || v < 1) return res.status(400).json({ error: 'version must be a positive number' });
113
+ doc.version = v;
114
+ } else {
115
+ doc.version = Number(doc.version || 1) + 1;
116
+ }
117
+
118
+ if (req.body?.isActive !== undefined) doc.isActive = Boolean(req.body.isActive);
119
+
120
+ await doc.save();
121
+ return res.json({ item: doc.toObject() });
122
+ } catch (error) {
123
+ console.error('[adminContextBlockDefinitions] update error:', error);
124
+ if (error?.name === 'ValidationError') return res.status(400).json({ error: error.message });
125
+ return res.status(500).json({ error: 'Failed to update context block definition' });
126
+ }
127
+ };
128
+
129
+ exports.remove = async (req, res) => {
130
+ try {
131
+ const code = normalizeCode(req.params?.code);
132
+ const doc = await ContextBlockDefinition.findOne({ code });
133
+ if (!doc) return res.status(404).json({ error: 'Context block definition not found' });
134
+
135
+ await ContextBlockDefinition.deleteOne({ _id: doc._id });
136
+ return res.json({ success: true });
137
+ } catch (error) {
138
+ console.error('[adminContextBlockDefinitions] remove error:', error);
139
+ return res.status(500).json({ error: 'Failed to delete context block definition' });
140
+ }
141
+ };