@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,27 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const blockDefinitionSchema = new mongoose.Schema(
4
+ {
5
+ code: {
6
+ type: String,
7
+ required: true,
8
+ unique: true,
9
+ index: true,
10
+ trim: true,
11
+ lowercase: true,
12
+ match: [/^[a-z][a-z0-9_-]{1,63}$/, 'Invalid block code'],
13
+ },
14
+
15
+ label: { type: String, required: true, trim: true },
16
+ description: { type: String, default: '' },
17
+
18
+ // Fields schema (server-side validation happens in services)
19
+ fields: { type: mongoose.Schema.Types.Mixed, default: {} },
20
+
21
+ version: { type: Number, default: 1 },
22
+ isActive: { type: Boolean, default: true, index: true },
23
+ },
24
+ { timestamps: true, collection: 'page_builder_block_definitions' },
25
+ );
26
+
27
+ module.exports = mongoose.models.BlockDefinition || mongoose.model('BlockDefinition', blockDefinitionSchema);
@@ -0,0 +1,14 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const blogAutomationLockSchema = new mongoose.Schema(
4
+ {
5
+ key: { type: String, required: true, unique: true, index: true },
6
+ lockedUntil: { type: Date, required: true },
7
+ ownerId: { type: String, required: true },
8
+ },
9
+ { timestamps: true, collection: 'blog_automation_locks' },
10
+ );
11
+
12
+ module.exports =
13
+ mongoose.models.BlogAutomationLock ||
14
+ mongoose.model('BlogAutomationLock', blogAutomationLockSchema);
@@ -0,0 +1,39 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const blogAutomationRunSchema = new mongoose.Schema(
4
+ {
5
+ status: {
6
+ type: String,
7
+ enum: ['queued', 'running', 'succeeded', 'failed', 'partial', 'skipped'],
8
+ default: 'queued',
9
+ index: true,
10
+ },
11
+ configId: {
12
+ type: String,
13
+ default: '',
14
+ index: true,
15
+ },
16
+ trigger: {
17
+ type: String,
18
+ enum: ['scheduled', 'manual'],
19
+ required: true,
20
+ index: true,
21
+ },
22
+ startedAt: { type: Date },
23
+ finishedAt: { type: Date },
24
+ configSnapshot: { type: mongoose.Schema.Types.Mixed, default: {} },
25
+ topic: { type: mongoose.Schema.Types.Mixed, default: {} },
26
+ results: { type: mongoose.Schema.Types.Mixed, default: {} },
27
+ steps: { type: [mongoose.Schema.Types.Mixed], default: [] },
28
+ error: { type: String, default: '' },
29
+ },
30
+ { timestamps: true, collection: 'blog_automation_runs' },
31
+ );
32
+
33
+ blogAutomationRunSchema.index({ createdAt: -1 });
34
+ blogAutomationRunSchema.index({ trigger: 1, createdAt: -1 });
35
+ blogAutomationRunSchema.index({ configId: 1, createdAt: -1 });
36
+
37
+ module.exports =
38
+ mongoose.models.BlogAutomationRun ||
39
+ mongoose.model('BlogAutomationRun', blogAutomationRunSchema);
@@ -0,0 +1,42 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const blogPostSchema = new mongoose.Schema(
4
+ {
5
+ title: { type: String, required: true },
6
+ slug: { type: String, required: true, index: true },
7
+ status: {
8
+ type: String,
9
+ enum: ['draft', 'scheduled', 'published', 'archived'],
10
+ default: 'draft',
11
+ index: true,
12
+ },
13
+ excerpt: { type: String, default: '' },
14
+ markdown: { type: String, default: '' },
15
+ html: { type: String, default: '' },
16
+ coverImageUrl: { type: String, default: '' },
17
+ category: { type: String, default: '' },
18
+ tags: { type: [String], default: [] },
19
+ authorName: { type: String, default: '' },
20
+ seoTitle: { type: String, default: '' },
21
+ seoDescription: { type: String, default: '' },
22
+ scheduledAt: { type: Date },
23
+ publishedAt: { type: Date },
24
+ },
25
+ { timestamps: true, collection: 'blog_posts' },
26
+ );
27
+
28
+ blogPostSchema.index({ status: 1, publishedAt: -1 });
29
+ blogPostSchema.index({ status: 1, scheduledAt: 1 });
30
+
31
+ // Enforce slug uniqueness among non-archived posts. Archived posts free up slugs.
32
+ blogPostSchema.index(
33
+ { slug: 1 },
34
+ {
35
+ unique: true,
36
+ partialFilterExpression: {
37
+ status: { $in: ['draft', 'scheduled', 'published'] },
38
+ },
39
+ },
40
+ );
41
+
42
+ module.exports = mongoose.models.BlogPost || mongoose.model('BlogPost', blogPostSchema);
@@ -0,0 +1,26 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const cacheEntrySchema = new mongoose.Schema(
4
+ {
5
+ namespace: { type: String, required: true, index: true },
6
+ key: { type: String, required: true, index: true },
7
+
8
+ value: { type: String, required: true },
9
+ atRestFormat: { type: String, enum: ['string', 'base64'], default: 'string', index: true },
10
+
11
+ sizeBytes: { type: Number, default: 0 },
12
+
13
+ expiresAt: { type: Date, default: null },
14
+
15
+ hits: { type: Number, default: 0 },
16
+ lastAccessAt: { type: Date, default: null },
17
+
18
+ source: { type: String, enum: ['offloaded', 'manual'], default: 'manual', index: true },
19
+ },
20
+ { timestamps: true, collection: 'cache_entries' },
21
+ );
22
+
23
+ cacheEntrySchema.index({ namespace: 1, key: 1 }, { unique: true });
24
+ cacheEntrySchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
25
+
26
+ module.exports = mongoose.models.CacheEntry || mongoose.model('CacheEntry', cacheEntrySchema);
@@ -0,0 +1,32 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const consoleEntrySchema = new mongoose.Schema(
4
+ {
5
+ hash: { type: String, required: true, unique: true, index: true },
6
+ method: { type: String, enum: ['debug', 'log', 'info', 'warn', 'error'], required: true, index: true },
7
+
8
+ messageTemplate: { type: String, default: '', maxlength: 500 },
9
+ topFrame: { type: String, default: '' },
10
+
11
+ enabled: { type: Boolean, default: true, index: true },
12
+ enabledExplicit: { type: Boolean, default: false, index: true },
13
+
14
+ persistToCache: { type: Boolean, default: false },
15
+ persistToDb: { type: Boolean, default: false },
16
+ persistExplicit: { type: Boolean, default: false, index: true },
17
+
18
+ tags: { type: [String], default: [], index: true },
19
+
20
+ countTotal: { type: Number, default: 0 },
21
+ firstSeenAt: { type: Date, default: Date.now },
22
+ lastSeenAt: { type: Date, default: Date.now, index: true },
23
+
24
+ lastSample: { type: mongoose.Schema.Types.Mixed, default: null },
25
+ },
26
+ { timestamps: true, collection: 'console_entries' },
27
+ );
28
+
29
+ consoleEntrySchema.index({ method: 1, lastSeenAt: -1 });
30
+ consoleEntrySchema.index({ enabled: 1, lastSeenAt: -1 });
31
+
32
+ module.exports = mongoose.models.ConsoleEntry || mongoose.model('ConsoleEntry', consoleEntrySchema);
@@ -0,0 +1,23 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const consoleLogSchema = new mongoose.Schema(
4
+ {
5
+ entryHash: { type: String, required: true, index: true },
6
+ method: { type: String, enum: ['debug', 'log', 'info', 'warn', 'error'], required: true, index: true },
7
+
8
+ message: { type: String, default: '', maxlength: 2000 },
9
+ argsPreview: { type: String, default: '', maxlength: 5000 },
10
+
11
+ tagsSnapshot: { type: [String], default: [], index: true },
12
+ requestId: { type: String, default: '', index: true },
13
+
14
+ createdAt: { type: Date, default: Date.now, index: true },
15
+ expiresAt: { type: Date, default: null },
16
+ },
17
+ { timestamps: false, collection: 'console_logs' },
18
+ );
19
+
20
+ consoleLogSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
21
+ consoleLogSchema.index({ entryHash: 1, createdAt: -1 });
22
+
23
+ module.exports = mongoose.models.ConsoleLog || mongoose.model('ConsoleLog', consoleLogSchema);
@@ -0,0 +1,33 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const contextBlockDefinitionSchema = new mongoose.Schema(
4
+ {
5
+ code: {
6
+ type: String,
7
+ required: true,
8
+ unique: true,
9
+ index: true,
10
+ trim: true,
11
+ lowercase: true,
12
+ match: [/^[a-z][a-z0-9_-]{1,63}$/, 'Invalid context block code'],
13
+ },
14
+
15
+ label: { type: String, required: true, trim: true },
16
+ description: { type: String, default: '' },
17
+
18
+ type: {
19
+ type: String,
20
+ required: true,
21
+ enum: ['context.db_query', 'context.service_invoke'],
22
+ index: true,
23
+ },
24
+
25
+ props: { type: mongoose.Schema.Types.Mixed, default: {} },
26
+
27
+ version: { type: Number, default: 1 },
28
+ isActive: { type: Boolean, default: true, index: true },
29
+ },
30
+ { timestamps: true, collection: 'page_builder_context_block_definitions' },
31
+ );
32
+
33
+ module.exports = mongoose.models.ContextBlockDefinition || mongoose.model('ContextBlockDefinition', contextBlockDefinitionSchema);
@@ -0,0 +1,47 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const cronExecutionSchema = new mongoose.Schema(
4
+ {
5
+ cronJobId: { type: mongoose.Schema.Types.ObjectId, ref: 'CronJob', required: true, index: true },
6
+
7
+ // Execution details
8
+ status: {
9
+ type: String,
10
+ enum: ['running', 'succeeded', 'failed', 'timed_out'],
11
+ default: 'running',
12
+ index: true
13
+ },
14
+ startedAt: { type: Date, default: Date.now, index: true },
15
+ finishedAt: { type: Date },
16
+ durationMs: { type: Number },
17
+
18
+ // Results
19
+ output: { type: String },
20
+ error: { type: String },
21
+
22
+ // HTTP specific
23
+ httpStatusCode: { type: Number },
24
+ httpResponseHeaders: { type: mongoose.Schema.Types.Mixed },
25
+
26
+ // Metadata
27
+ triggeredAt: { type: Date, required: true }, // When it was supposed to run
28
+ actualRunAt: { type: Date, default: Date.now }, // When it actually started
29
+ },
30
+ { timestamps: true, collection: 'cron_executions' },
31
+ );
32
+
33
+ // Index for efficient queries
34
+ cronExecutionSchema.index({ cronJobId: 1, startedAt: -1 });
35
+ cronExecutionSchema.index({ status: 1, startedAt: -1 });
36
+
37
+ // Calculate duration before saving
38
+ cronExecutionSchema.pre('save', function preSave(next) {
39
+ if (this.isModified('finishedAt') && this.finishedAt && this.startedAt) {
40
+ this.durationMs = this.finishedAt.getTime() - this.startedAt.getTime();
41
+ }
42
+ next();
43
+ });
44
+
45
+ module.exports =
46
+ mongoose.models.CronExecution ||
47
+ mongoose.model('CronExecution', cronExecutionSchema);
@@ -0,0 +1,70 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const envVarSchema = new mongoose.Schema(
4
+ {
5
+ key: { type: String, required: true },
6
+ value: { type: String, required: true },
7
+ },
8
+ { _id: false },
9
+ );
10
+
11
+ const httpHeaderSchema = new mongoose.Schema(
12
+ {
13
+ key: { type: String, required: true },
14
+ value: { type: String, required: true },
15
+ },
16
+ { _id: false },
17
+ );
18
+
19
+ const httpAuthSchema = new mongoose.Schema(
20
+ {
21
+ type: { type: String, enum: ['bearer', 'basic', 'none'], default: 'none' },
22
+ token: { type: String },
23
+ username: { type: String },
24
+ password: { type: String },
25
+ },
26
+ { _id: false },
27
+ );
28
+
29
+ const cronJobSchema = new mongoose.Schema(
30
+ {
31
+ name: { type: String, required: true },
32
+ description: { type: String, default: '' },
33
+
34
+ // Schedule configuration
35
+ cronExpression: { type: String, required: true },
36
+ timezone: { type: String, default: 'UTC' },
37
+ enabled: { type: Boolean, default: true, index: true },
38
+ nextRunAt: { type: Date, index: true },
39
+
40
+ // Task configuration
41
+ taskType: { type: String, enum: ['script', 'http'], required: true },
42
+
43
+ // Script task fields
44
+ scriptId: { type: mongoose.Schema.Types.ObjectId, ref: 'ScriptDefinition' },
45
+ scriptEnv: { type: [envVarSchema], default: [] },
46
+
47
+ // HTTP task fields
48
+ httpMethod: { type: String, enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], default: 'GET' },
49
+ httpUrl: { type: String, required: function() { return this.taskType === 'http'; } },
50
+ httpHeaders: { type: [httpHeaderSchema], default: [] },
51
+ httpBody: { type: String },
52
+ httpBodyType: { type: String, enum: ['json', 'raw', 'form'], default: 'raw' },
53
+ httpAuth: { type: httpAuthSchema, default: () => ({}) },
54
+
55
+ // Common fields
56
+ timeoutMs: { type: Number, default: 300000 }, // 5 minutes
57
+
58
+ // Metadata
59
+ createdBy: { type: String, required: true },
60
+ },
61
+ { timestamps: true, collection: 'cron_jobs' },
62
+ );
63
+
64
+ // Index for efficient queries
65
+ cronJobSchema.index({ taskType: 1, enabled: 1 });
66
+ cronJobSchema.index({ nextRunAt: 1, enabled: 1 });
67
+
68
+ module.exports =
69
+ mongoose.models.CronJob ||
70
+ mongoose.model('CronJob', cronJobSchema);
@@ -0,0 +1,75 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const experimentVariantSchema = new mongoose.Schema(
4
+ {
5
+ key: { type: String, required: true },
6
+ weight: { type: Number, default: 0 },
7
+ configSlug: { type: String, default: '' },
8
+ },
9
+ { _id: false },
10
+ );
11
+
12
+ const metricDefinitionSchema = new mongoose.Schema(
13
+ {
14
+ key: { type: String, required: true },
15
+ kind: { type: String, enum: ['count', 'sum', 'avg', 'rate'], default: 'count' },
16
+ numeratorEventKey: { type: String, default: '' },
17
+ denominatorEventKey: { type: String, default: '' },
18
+ objective: { type: String, enum: ['maximize', 'minimize'], default: 'maximize' },
19
+ },
20
+ { _id: false },
21
+ );
22
+
23
+ const winnerPolicySchema = new mongoose.Schema(
24
+ {
25
+ mode: { type: String, enum: ['manual', 'automatic'], default: 'manual' },
26
+ pickAfterMs: { type: Number, default: 0 },
27
+ minAssignments: { type: Number, default: 0 },
28
+ minExposures: { type: Number, default: 0 },
29
+ minConversions: { type: Number, default: 0 },
30
+ statMethod: { type: String, enum: ['simple_rate', 'bayesian_beta'], default: 'simple_rate' },
31
+ overrideWinnerVariantKey: { type: String, default: '' },
32
+ },
33
+ { _id: false },
34
+ );
35
+
36
+ const experimentSchema = new mongoose.Schema(
37
+ {
38
+ organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
39
+
40
+ code: { type: String, required: true },
41
+ name: { type: String, default: '' },
42
+ description: { type: String, default: '' },
43
+
44
+ status: { type: String, enum: ['draft', 'running', 'paused', 'completed'], default: 'draft', index: true },
45
+
46
+ startedAt: { type: Date, default: null },
47
+ endsAt: { type: Date, default: null },
48
+
49
+ assignment: {
50
+ unit: { type: String, enum: ['subjectId'], default: 'subjectId' },
51
+ sticky: { type: Boolean, default: true },
52
+ salt: { type: String, default: '' },
53
+ },
54
+
55
+ variants: { type: [experimentVariantSchema], default: [] },
56
+
57
+ primaryMetric: { type: metricDefinitionSchema, required: true },
58
+ secondaryMetrics: { type: [metricDefinitionSchema], default: [] },
59
+
60
+ winnerPolicy: { type: winnerPolicySchema, default: () => ({}) },
61
+
62
+ winnerVariantKey: { type: String, default: '' },
63
+ winnerDecidedAt: { type: Date, default: null },
64
+ winnerReason: { type: String, default: '' },
65
+
66
+ createdByUserId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null },
67
+ updatedByUserId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null },
68
+ },
69
+ { timestamps: true, collection: 'experiments' },
70
+ );
71
+
72
+ experimentSchema.index({ organizationId: 1, code: 1 }, { unique: true });
73
+ experimentSchema.index({ status: 1, startedAt: 1 });
74
+
75
+ module.exports = mongoose.models.Experiment || mongoose.model('Experiment', experimentSchema);
@@ -0,0 +1,23 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const experimentAssignmentSchema = new mongoose.Schema(
4
+ {
5
+ experimentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Experiment', required: true, index: true },
6
+ organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
7
+
8
+ subjectKey: { type: String, required: true },
9
+
10
+ variantKey: { type: String, required: true },
11
+ assignedAt: { type: Date, default: () => new Date() },
12
+
13
+ context: { type: mongoose.Schema.Types.Mixed, default: {} },
14
+ },
15
+ { timestamps: true, collection: 'experiment_assignments' },
16
+ );
17
+
18
+ experimentAssignmentSchema.index({ experimentId: 1, subjectKey: 1 }, { unique: true });
19
+ experimentAssignmentSchema.index({ organizationId: 1, subjectKey: 1 });
20
+
21
+ module.exports =
22
+ mongoose.models.ExperimentAssignment ||
23
+ mongoose.model('ExperimentAssignment', experimentAssignmentSchema);
@@ -0,0 +1,26 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const experimentEventSchema = new mongoose.Schema(
4
+ {
5
+ experimentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Experiment', required: true, index: true },
6
+ organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
7
+
8
+ subjectKey: { type: String, required: true },
9
+
10
+ variantKey: { type: String, required: true, index: true },
11
+
12
+ eventKey: { type: String, required: true, index: true },
13
+ value: { type: Number, default: 1 },
14
+
15
+ ts: { type: Date, required: true, index: true },
16
+
17
+ meta: { type: mongoose.Schema.Types.Mixed, default: {} },
18
+ },
19
+ { timestamps: true, collection: 'experiment_events' },
20
+ );
21
+
22
+ experimentEventSchema.index({ experimentId: 1, ts: 1 });
23
+ experimentEventSchema.index({ organizationId: 1, ts: 1 });
24
+ experimentEventSchema.index({ experimentId: 1, eventKey: 1, ts: 1 });
25
+
26
+ module.exports = mongoose.models.ExperimentEvent || mongoose.model('ExperimentEvent', experimentEventSchema);
@@ -0,0 +1,30 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const experimentMetricBucketSchema = new mongoose.Schema(
4
+ {
5
+ experimentId: { type: mongoose.Schema.Types.ObjectId, ref: 'Experiment', required: true, index: true },
6
+ organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', default: null, index: true },
7
+
8
+ variantKey: { type: String, required: true, index: true },
9
+ metricKey: { type: String, required: true, index: true },
10
+
11
+ bucketStart: { type: Date, required: true, index: true },
12
+ bucketMs: { type: Number, required: true },
13
+
14
+ count: { type: Number, default: 0 },
15
+ sum: { type: Number, default: 0 },
16
+ sumSq: { type: Number, default: 0 },
17
+ min: { type: Number, default: null },
18
+ max: { type: Number, default: null },
19
+ },
20
+ { timestamps: true, collection: 'experiment_metric_buckets' },
21
+ );
22
+
23
+ experimentMetricBucketSchema.index(
24
+ { experimentId: 1, variantKey: 1, metricKey: 1, bucketStart: 1, bucketMs: 1 },
25
+ { unique: true },
26
+ );
27
+
28
+ module.exports =
29
+ mongoose.models.ExperimentMetricBucket ||
30
+ mongoose.model('ExperimentMetricBucket', experimentMetricBucketSchema);
@@ -0,0 +1,49 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const externalDbConnectionSchema = new mongoose.Schema(
4
+ {
5
+ name: {
6
+ type: String,
7
+ required: true,
8
+ unique: true,
9
+ trim: true,
10
+ index: true,
11
+ },
12
+ type: {
13
+ type: String,
14
+ required: true,
15
+ enum: ['mongo', 'mysql'],
16
+ index: true,
17
+ },
18
+ enabled: {
19
+ type: Boolean,
20
+ default: true,
21
+ index: true,
22
+ },
23
+
24
+ // Non-secret (safe to return)
25
+ uriMasked: {
26
+ type: String,
27
+ default: null,
28
+ },
29
+
30
+ // Encrypted at rest (NEVER return decrypted)
31
+ uriEncrypted: {
32
+ type: mongoose.Schema.Types.Mixed,
33
+ required: true,
34
+ },
35
+ },
36
+ {
37
+ timestamps: true,
38
+ },
39
+ );
40
+
41
+ externalDbConnectionSchema.set('toJSON', {
42
+ transform: (_doc, ret) => {
43
+ delete ret.uriEncrypted;
44
+ delete ret.__v;
45
+ return ret;
46
+ },
47
+ });
48
+
49
+ module.exports = mongoose.model('ExternalDbConnection', externalDbConnectionSchema);
@@ -0,0 +1,22 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const fileEntrySchema = new mongoose.Schema(
4
+ {
5
+ orgId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },
6
+ driveType: { type: String, enum: ['user', 'group', 'org'], required: true, index: true },
7
+ driveId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
8
+ parentPath: { type: String, required: true, default: '/', index: true },
9
+ name: { type: String, required: true },
10
+ assetId: { type: mongoose.Schema.Types.ObjectId, ref: 'Asset', required: true, index: true },
11
+ visibility: { type: String, enum: ['public', 'private'], required: true, default: 'private', index: true },
12
+ deletedAt: { type: Date, default: null, index: true },
13
+ },
14
+ { timestamps: true, collection: 'file_entries' },
15
+ );
16
+
17
+ fileEntrySchema.index(
18
+ { orgId: 1, driveType: 1, driveId: 1, parentPath: 1, name: 1 },
19
+ { unique: true, partialFilterExpression: { deletedAt: null } },
20
+ );
21
+
22
+ module.exports = mongoose.models.FileEntry || mongoose.model('FileEntry', fileEntrySchema);
@@ -4,8 +4,7 @@ const globalSettingSchema = new mongoose.Schema({
4
4
  key: {
5
5
  type: String,
6
6
  required: true,
7
- unique: true,
8
- index: true
7
+ unique: true
9
8
  },
10
9
  value: {
11
10
  type: String,
@@ -0,0 +1,57 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ const actionResultSchema = new mongoose.Schema(
4
+ {
5
+ actionType: { type: String, default: '' },
6
+ status: { type: String, enum: ['succeeded', 'failed'], required: true },
7
+ output: { type: String, default: '' },
8
+ error: { type: String, default: '' },
9
+ },
10
+ { _id: false },
11
+ );
12
+
13
+ const healthAutoHealAttemptSchema = new mongoose.Schema(
14
+ {
15
+ healthCheckId: {
16
+ type: mongoose.Schema.Types.ObjectId,
17
+ ref: 'HealthCheck',
18
+ required: true,
19
+ index: true,
20
+ },
21
+ incidentId: {
22
+ type: mongoose.Schema.Types.ObjectId,
23
+ ref: 'HealthIncident',
24
+ required: true,
25
+ index: true,
26
+ },
27
+
28
+ attemptNumber: { type: Number, required: true },
29
+
30
+ status: {
31
+ type: String,
32
+ enum: ['running', 'succeeded', 'failed'],
33
+ default: 'running',
34
+ index: true,
35
+ },
36
+
37
+ startedAt: { type: Date, default: Date.now, index: true },
38
+ finishedAt: { type: Date },
39
+ durationMs: { type: Number },
40
+
41
+ actionResults: { type: [actionResultSchema], default: [] },
42
+ },
43
+ { timestamps: true, collection: 'health_autoheal_attempts' },
44
+ );
45
+
46
+ healthAutoHealAttemptSchema.index({ incidentId: 1, startedAt: -1 });
47
+
48
+ healthAutoHealAttemptSchema.pre('save', function preSave(next) {
49
+ if (this.isModified('finishedAt') && this.finishedAt && this.startedAt) {
50
+ this.durationMs = this.finishedAt.getTime() - this.startedAt.getTime();
51
+ }
52
+ next();
53
+ });
54
+
55
+ module.exports =
56
+ mongoose.models.HealthAutoHealAttempt ||
57
+ mongoose.model('HealthAutoHealAttempt', healthAutoHealAttemptSchema);