webspresso 0.0.96 → 0.0.98

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 (121) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  7. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  10. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/admin-password.js +2 -1
  14. package/bin/commands/admin-setup.js +1 -1
  15. package/bin/commands/agents-init.js +51 -11
  16. package/bin/commands/analytics-prune.js +57 -0
  17. package/bin/commands/api.js +207 -15
  18. package/bin/commands/audit-prune.js +1 -1
  19. package/bin/commands/build.js +99 -0
  20. package/bin/commands/dev.js +64 -35
  21. package/bin/commands/doctor.js +26 -0
  22. package/bin/commands/email-prune.js +47 -0
  23. package/bin/commands/favicon-generate.js +1 -1
  24. package/bin/commands/make-job.js +86 -0
  25. package/bin/commands/make-model.js +250 -0
  26. package/bin/commands/new.js +45 -28
  27. package/bin/commands/openapi.js +102 -0
  28. package/bin/commands/orm-map.js +5 -4
  29. package/bin/commands/page.js +6 -3
  30. package/bin/commands/polar-migrate.js +1 -1
  31. package/bin/commands/queue-prune.js +59 -0
  32. package/bin/commands/routes.js +52 -0
  33. package/bin/commands/seed.js +1 -1
  34. package/bin/commands/service.js +6 -3
  35. package/bin/commands/typegen.js +106 -0
  36. package/bin/commands/upload-prune.js +79 -0
  37. package/bin/webspresso.js +22 -0
  38. package/core/applySchema.js +13 -6
  39. package/core/auth/hash.js +31 -17
  40. package/core/compression/middleware.js +23 -17
  41. package/core/openapi/build-from-api-routes.js +160 -29
  42. package/core/orm/cache/fingerprint.js +15 -1
  43. package/core/orm/cache/memory-provider.js +27 -2
  44. package/core/orm/index.js +12 -7
  45. package/core/orm/model.js +49 -7
  46. package/core/queue/adapters/database.js +94 -0
  47. package/core/queue/adapters/memory.js +62 -0
  48. package/core/queue/adapters/redis.js +80 -0
  49. package/core/queue/manager.js +123 -0
  50. package/core/ssr/stream.js +11 -0
  51. package/index.d.ts +660 -10
  52. package/index.js +44 -96
  53. package/package.json +32 -12
  54. package/plugins/admin-panel/api.js +279 -17
  55. package/plugins/admin-panel/app.js +124 -31
  56. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  57. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  58. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  59. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  60. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  61. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  62. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  63. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  64. package/plugins/admin-panel/core/admin-module.js +13 -7
  65. package/plugins/admin-panel/core/api-extensions.js +34 -2
  66. package/plugins/admin-panel/core/registry.js +96 -16
  67. package/plugins/admin-panel/index.js +96 -0
  68. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  69. package/plugins/admin-panel/modules/admin-users.js +16 -1
  70. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  71. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  72. package/plugins/admin-panel/modules/dashboard.js +283 -0
  73. package/plugins/admin-panel/modules/menu.js +56 -18
  74. package/plugins/admin-panel/modules/user-management.js +24 -6
  75. package/plugins/audit-log/admin-component.js +11 -0
  76. package/plugins/audit-log/api-handlers.js +34 -0
  77. package/plugins/audit-log/index.js +129 -3
  78. package/plugins/audit-log/purge.js +60 -10
  79. package/plugins/basic-auth/index.js +3 -1
  80. package/plugins/content/client/inline-edit.js +12 -12
  81. package/plugins/cors.js +6 -7
  82. package/plugins/dashboard/app.js +152 -27
  83. package/plugins/dashboard/index.js +143 -39
  84. package/plugins/dashboard/styles.js +78 -0
  85. package/plugins/data-exchange/export-xlsx.js +9 -2
  86. package/plugins/data-exchange/import.js +9 -2
  87. package/plugins/data-exchange/index.js +6 -3
  88. package/plugins/email/index.js +132 -2
  89. package/plugins/email/purge.js +60 -10
  90. package/plugins/file-manager/core.js +1 -1
  91. package/plugins/file-manager/security.js +33 -23
  92. package/plugins/index.js +49 -87
  93. package/plugins/mcp/transports/sse.js +4 -2
  94. package/plugins/orm-cache-admin/index.js +4 -2
  95. package/plugins/queue/index.js +11 -0
  96. package/plugins/schema-explorer/admin-component.js +714 -0
  97. package/plugins/schema-explorer/relations-graph.js +205 -0
  98. package/plugins/schema-explorer.js +248 -69
  99. package/plugins/site-analytics/admin-component.js +30 -8
  100. package/plugins/site-analytics/api-handlers.js +42 -0
  101. package/plugins/site-analytics/index.js +147 -2
  102. package/plugins/site-analytics/purge.js +168 -0
  103. package/plugins/swagger.js +261 -13
  104. package/plugins/upload/index.js +26 -0
  105. package/plugins/upload/orphan-pruner.js +292 -0
  106. package/src/api/api-loader.js +3 -1
  107. package/src/bundle/index.js +517 -0
  108. package/src/bundle/manifest.js +165 -0
  109. package/src/dev/banner.js +296 -0
  110. package/src/dev/colors.js +70 -0
  111. package/src/dev/index.js +62 -0
  112. package/src/dev/profiler.js +354 -0
  113. package/src/dev/typegen.js +497 -0
  114. package/src/dev-reload/index.js +611 -0
  115. package/src/file-router.js +213 -63
  116. package/src/helpers.js +958 -98
  117. package/src/njk-frontmatter.js +12 -0
  118. package/src/pages/page-loader.js +61 -11
  119. package/src/server.js +629 -63
  120. package/src/services/discovery.js +6 -1
  121. package/src/services/index.js +6 -13
@@ -3,24 +3,74 @@
3
3
  * @module plugins/email/purge
4
4
  */
5
5
 
6
+ 'use strict';
7
+
6
8
  /**
7
9
  * @param {import('knex').Knex} knex
8
- * @param {Object} options
10
+ * @param {Object} [options={}]
9
11
  * @param {string} [options.tableName='email_logs']
10
- * @param {Date|string} options.olderThan
11
- * @returns {Promise<number>}
12
+ * @param {Date|string|number} [options.olderThan] - Rows with created_at strictly before this are removed
13
+ * @param {number} [options.days] - Number of days of email logs to keep
14
+ * @param {number} [options.retentionDays=90] - Alias for days
15
+ * @returns {Promise<number>} Deleted row count
12
16
  */
13
- async function purgeEmailLogs(knex, options) {
17
+ async function purgeEmailLogs(knex, options = {}) {
18
+ if (!knex) {
19
+ throw new Error('purgeEmailLogs requires a database (knex) instance');
20
+ }
21
+
14
22
  const tableName = options.tableName || 'email_logs';
15
- const olderThan = options.olderThan instanceof Date
16
- ? options.olderThan
17
- : new Date(options.olderThan);
18
23
 
19
- if (Number.isNaN(olderThan.getTime())) {
20
- throw new Error('purgeEmailLogs: invalid olderThan date');
24
+ let olderThan;
25
+ if (options.olderThan !== undefined) {
26
+ olderThan = options.olderThan instanceof Date
27
+ ? options.olderThan
28
+ : new Date(options.olderThan);
29
+
30
+ if (Number.isNaN(olderThan.getTime())) {
31
+ throw new Error('purgeEmailLogs: invalid olderThan date');
32
+ }
33
+ } else {
34
+ const rawDays = options.days !== undefined ? options.days : (options.retentionDays !== undefined ? options.retentionDays : 90);
35
+ const days = typeof rawDays === 'number' ? rawDays : Number(rawDays);
36
+ if (Number.isNaN(days) || days <= 0) {
37
+ throw new Error('purgeEmailLogs: days must be a positive number');
38
+ }
39
+ olderThan = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
40
+ }
41
+
42
+ let hasTable = true;
43
+ if (knex.schema && typeof knex.schema.hasTable === 'function') {
44
+ try {
45
+ hasTable = await knex.schema.hasTable(tableName);
46
+ } catch {
47
+ hasTable = false;
48
+ }
49
+ }
50
+
51
+ if (!hasTable) {
52
+ return 0;
53
+ }
54
+
55
+ const client = knex?.client?.config?.client;
56
+ const isSqlite = client === 'sqlite3' || client === 'better-sqlite3';
57
+
58
+ let query = knex(tableName);
59
+ if (isSqlite) {
60
+ const iso = olderThan.toISOString();
61
+ const cutoffMs = olderThan.getTime();
62
+ query = query.where((builder) => {
63
+ builder.whereRaw(
64
+ "((typeof(created_at) = 'integer' and created_at < ?) or (typeof(created_at) = 'text' and created_at < ?))",
65
+ [cutoffMs, iso]
66
+ );
67
+ });
68
+ } else {
69
+ query = query.where('created_at', '<', olderThan);
21
70
  }
22
71
 
23
- return knex(tableName).where('created_at', '<', olderThan).delete();
72
+ const deleted = await query.delete();
73
+ return Number(deleted) || 0;
24
74
  }
25
75
 
26
76
  module.exports = {
@@ -217,7 +217,7 @@ function buildBreadcrumbs(relPath) {
217
217
  const breadcrumbs = [{ name: 'Root', path: '' }];
218
218
  if (!relPath) return breadcrumbs;
219
219
 
220
- const segments = relPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
220
+ const segments = relPath.replace(/\\/g, '/').split('/').filter(Boolean);
221
221
  let current = '';
222
222
 
223
223
  for (const seg of segments) {
@@ -4,6 +4,7 @@
4
4
  * @module plugins/file-manager/security
5
5
  */
6
6
 
7
+ const sanitizeHtml = require('sanitize-html');
7
8
  const { SecurityError, ValidationError } = require('../../core/errors');
8
9
 
9
10
  /**
@@ -164,6 +165,37 @@ function validateMagicBytes(buffer, ext) {
164
165
  return true;
165
166
  }
166
167
 
168
+ const SVG_SANITIZE_OPTIONS = {
169
+ allowedTags: [
170
+ 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',
171
+ 'ellipse', 'text', 'tspan', 'defs', 'clipPath', 'mask', 'pattern',
172
+ 'image', 'linearGradient', 'radialGradient', 'stop', 'use', 'symbol',
173
+ 'title', 'desc', 'a',
174
+ ],
175
+ allowedAttributes: {
176
+ '*': [
177
+ 'id', 'class', 'style', 'xml:space', 'xmlns', 'xmlns:xlink', 'xmlns:svg',
178
+ 'version', 'viewbox', 'viewBox', 'width', 'height', 'x', 'y', 'dx', 'dy',
179
+ 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'd', 'points',
180
+ 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
181
+ 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'opacity',
182
+ 'fill-opacity', 'stroke-opacity', 'fill-rule', 'clip-rule',
183
+ 'transform', 'font-family', 'font-size', 'font-weight', 'text-anchor',
184
+ 'clip-path', 'mask', 'gradientUnits', 'gradientTransform', 'offset',
185
+ 'stop-color', 'stop-opacity', 'href', 'xlink:href', 'preserveAspectRatio',
186
+ ],
187
+ },
188
+ allowedSchemes: ['http', 'https'],
189
+ allowedSchemesByTag: {
190
+ a: ['http', 'https', 'mailto'],
191
+ image: ['http', 'https', 'data'],
192
+ use: ['#'],
193
+ },
194
+ allowProtocolRelative: false,
195
+ selfClosing: ['rect', 'circle', 'line', 'polyline', 'polygon', 'path', 'stop', 'use', 'image', 'ellipse'],
196
+ disallowedTagsMode: 'discard',
197
+ };
198
+
167
199
  /**
168
200
  * Sanitize SVG content against Stored XSS attacks.
169
201
  * Strips <script>, <foreignObject>, <iframe>, <embed>, <object> and on* event handlers.
@@ -172,29 +204,7 @@ function validateMagicBytes(buffer, ext) {
172
204
  */
173
205
  function sanitizeSvgContent(content) {
174
206
  const str = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
175
-
176
- // Quick check: if no dangerous keywords, return original buffer directly for maximum speed
177
- if (!/<(script|foreignobject|iframe|embed|object|meta|link|style)/i.test(str) &&
178
- !/\s+on[a-z]+\s*=/i.test(str) &&
179
- !/javascript:/i.test(str) &&
180
- !/data:text\/html/i.test(str)) {
181
- return Buffer.isBuffer(content) ? content : Buffer.from(str, 'utf8');
182
- }
183
-
184
- let sanitized = str
185
- // Remove script tags and content
186
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>/gi, '')
187
- // Remove dangerous containers
188
- .replace(/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject\s*>/gi, '')
189
- .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe\s*>/gi, '')
190
- .replace(/<embed\b[^>]*>/gi, '')
191
- .replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object\s*>/gi, '')
192
- // Remove inline event handlers (onload, onclick, onerror, onmouseover, etc.)
193
- .replace(/\s+on[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
194
- // Remove javascript: and data:text/html URIs in href and xlink:href
195
- .replace(/(?:href|xlink:href)\s*=\s*(?:"\s*javascript:[^"]*"|'\s*javascript:[^']*')/gi, 'href=""')
196
- .replace(/(?:href|xlink:href)\s*=\s*(?:"\s*data:text\/html[^"]*"|'\s*data:text\/html[^']*')/gi, 'href=""');
197
-
207
+ const sanitized = sanitizeHtml(str, SVG_SANITIZE_OPTIONS);
198
208
  return Buffer.from(sanitized, 'utf8');
199
209
  }
200
210
 
package/plugins/index.js CHANGED
@@ -3,94 +3,56 @@
3
3
  * Export all built-in plugins
4
4
  */
5
5
 
6
- const sitemapPlugin = require('./sitemap');
7
- const analyticsPlugin = require('./analytics');
8
- const dashboardPlugin = require('./dashboard/index');
9
- const schemaExplorerPlugin = require('./schema-explorer');
10
- const adminPanelPlugin = require('./admin-panel');
11
- const seoCheckerPlugin = require('./seo-checker');
12
- const siteAnalyticsPlugin = require('./site-analytics');
13
- const auditLogPlugin = require('./audit-log');
14
- const recaptchaPlugin = require('./recaptcha');
15
- const swaggerPlugin = require('./swagger');
16
- const healthCheckPlugin = require('./health-check');
17
- const restResourcePlugin = require('./rest-resources');
18
- const ormCacheAdminPlugin = require('./orm-cache-admin');
19
- const { uploadPlugin, createLocalFileProvider } = require('./upload');
20
- /** Register after adminPanelPlugin (same db + adminPath) for session and routes. */
21
- const { dataExchangePlugin } = require('./data-exchange');
22
- const { redirectPlugin } = require('./redirect');
23
- const { rateLimitPlugin } = require('./rate-limit');
24
- const emailPlugin = require('./email');
25
- const contentPlugin = require('./content');
26
- const csrfPlugin = require('./csrf');
27
- const corsPlugin = require('./cors');
28
- const { basicAuthPlugin, createBasicAuthMiddleware } = require('./basic-auth');
29
- const {
30
- realtimePlugin,
31
- realtime,
32
- createRealtime,
33
- websocket,
34
- createWebSocketAdapter,
35
- sse,
36
- createSseAdapter,
37
- socketIo,
38
- createSocketIoAdapter,
39
- redis,
40
- createRedisAdapter,
41
- } = require('./realtime');
42
- const { mcpPlugin, createMcpServer, McpServer } = require('./mcp');
43
- const polarPlugin = require('./polar');
44
- const fileManagerPlugin = require('./file-manager');
45
- const { xlsxPlugin, xlsx } = require('./xlsx');
46
- const { csvPlugin, csv } = require('./csv');
47
-
48
6
  module.exports = {
49
- sitemapPlugin,
50
- analyticsPlugin,
51
- dashboardPlugin,
52
- schemaExplorerPlugin,
53
- adminPanelPlugin,
54
- seoCheckerPlugin,
55
- siteAnalyticsPlugin,
56
- auditLogPlugin,
57
- recaptchaPlugin,
58
- swaggerPlugin,
59
- healthCheckPlugin,
60
- restResourcePlugin,
61
- ormCacheAdminPlugin,
62
- uploadPlugin,
63
- createLocalFileProvider,
64
- dataExchangePlugin,
65
- redirectPlugin,
66
- rateLimitPlugin,
67
- emailPlugin,
68
- contentPlugin,
69
- csrfPlugin,
70
- corsPlugin,
71
- basicAuthPlugin,
72
- createBasicAuthMiddleware,
73
- realtimePlugin,
74
- realtime,
75
- createRealtime,
76
- websocket,
77
- createWebSocketAdapter,
78
- sse,
79
- createSseAdapter,
80
- socketIo,
81
- createSocketIoAdapter,
82
- redis,
83
- createRedisAdapter,
84
- queuePlugin: require('./queue'),
85
- mcpPlugin,
86
- createMcpServer,
87
- McpServer,
88
- polarPlugin,
89
- fileManagerPlugin,
90
- xlsxPlugin,
91
- xlsx,
92
- csvPlugin,
93
- csv,
7
+ get sitemapPlugin() { return require('./sitemap'); },
8
+ get analyticsPlugin() { return require('./analytics'); },
9
+ get dashboardPlugin() { return require('./dashboard/index'); },
10
+ get devToolbarPlugin() { return require('./dashboard/index').devToolbarPlugin; },
11
+ get devtoolsPlugin() { return require('./dashboard/index').devtoolsPlugin; },
12
+ get schemaExplorerPlugin() { return require('./schema-explorer'); },
13
+ get adminPanelPlugin() { return require('./admin-panel'); },
14
+ get seoCheckerPlugin() { return require('./seo-checker'); },
15
+ get siteAnalyticsPlugin() { return require('./site-analytics'); },
16
+ get auditLogPlugin() { return require('./audit-log'); },
17
+ get recaptchaPlugin() { return require('./recaptcha'); },
18
+ get swaggerPlugin() { return require('./swagger'); },
19
+ get openapiPlugin() { return require('./swagger'); },
20
+ get apiDocsPlugin() { return require('./swagger'); },
21
+ get healthCheckPlugin() { return require('./health-check'); },
22
+ get restResourcePlugin() { return require('./rest-resources'); },
23
+ get ormCacheAdminPlugin() { return require('./orm-cache-admin'); },
24
+ get uploadPlugin() { return require('./upload').uploadPlugin; },
25
+ get createLocalFileProvider() { return require('./upload').createLocalFileProvider; },
26
+ get dataExchangePlugin() { return require('./data-exchange').dataExchangePlugin; },
27
+ get redirectPlugin() { return require('./redirect').redirectPlugin; },
28
+ get rateLimitPlugin() { return require('./rate-limit').rateLimitPlugin; },
29
+ get emailPlugin() { return require('./email'); },
30
+ get contentPlugin() { return require('./content'); },
31
+ get csrfPlugin() { return require('./csrf'); },
32
+ get corsPlugin() { return require('./cors'); },
33
+ get basicAuthPlugin() { return require('./basic-auth').basicAuthPlugin; },
34
+ get createBasicAuthMiddleware() { return require('./basic-auth').createBasicAuthMiddleware; },
35
+ get realtimePlugin() { return require('./realtime').realtimePlugin; },
36
+ get realtime() { return require('./realtime').realtime; },
37
+ get createRealtime() { return require('./realtime').createRealtime; },
38
+ get websocket() { return require('./realtime').websocket; },
39
+ get createWebSocketAdapter() { return require('./realtime').createWebSocketAdapter; },
40
+ get sse() { return require('./realtime').sse; },
41
+ get createSseAdapter() { return require('./realtime').createSseAdapter; },
42
+ get socketIo() { return require('./realtime').socketIo; },
43
+ get createSocketIoAdapter() { return require('./realtime').createSocketIoAdapter; },
44
+ get redis() { return require('./realtime').redis; },
45
+ get createRedisAdapter() { return require('./realtime').createRedisAdapter; },
46
+ get queuePlugin() { return require('./queue'); },
47
+ get mcpPlugin() { return require('./mcp').mcpPlugin; },
48
+ get createMcpServer() { return require('./mcp').createMcpServer; },
49
+ get McpServer() { return require('./mcp').McpServer; },
50
+ get polarPlugin() { return require('./polar'); },
51
+ get fileManagerPlugin() { return require('./file-manager'); },
52
+ get xlsxPlugin() { return require('./xlsx').xlsxPlugin; },
53
+ get xlsx() { return require('./xlsx').xlsx; },
54
+ get csvPlugin() { return require('./csv').csvPlugin; },
55
+ get csv() { return require('./csv').csv; },
94
56
  };
95
57
 
96
58
 
@@ -18,8 +18,10 @@ const { createJsonRpcError, ERROR_CODES } = require('../server');
18
18
  */
19
19
  function mountSseTransport(options) {
20
20
  const { app, server, path: basePath = '/_mcp', authMiddleware } = options;
21
-
22
- const normalizedBase = basePath.replace(/\/+$/, '');
21
+ let normalizedBase = String(basePath || '/_mcp');
22
+ while (normalizedBase.length > 1 && normalizedBase.endsWith('/')) {
23
+ normalizedBase = normalizedBase.slice(0, -1);
24
+ }
23
25
  const sessions = new Map();
24
26
 
25
27
  const middlewareChain = authMiddleware ? [authMiddleware] : [];
@@ -11,7 +11,8 @@ const { generateOrmCacheAdminComponent } = require('./admin-component');
11
11
  * @param {import('../../core/orm/types').DatabaseInstance} options.db - From createDatabase({ cache: true })
12
12
  */
13
13
  function ormCacheAdminPlugin(options = {}) {
14
- const { db } = options;
14
+ const { db, title, adminTitle } = options;
15
+ const pageTitle = title || adminTitle || 'ORM Cache';
15
16
 
16
17
  if (!db) {
17
18
  throw new Error('orm-cache-admin plugin requires `db` (database instance from createDatabase)');
@@ -41,9 +42,10 @@ function ormCacheAdminPlugin(options = {}) {
41
42
 
42
43
  adminApi.registerModule({
43
44
  id: 'orm-cache',
45
+ title: pageTitle,
44
46
  pages: [{
45
47
  id: 'orm-cache',
46
- title: 'ORM Cache',
48
+ title: pageTitle,
47
49
  path: '/orm-cache',
48
50
  icon: 'database',
49
51
  component: generateOrmCacheAdminComponent(),
@@ -72,6 +72,12 @@ function queuePlugin(options = {}) {
72
72
  pollInterval,
73
73
  jobsDir,
74
74
  autoStart,
75
+ retention: options.retention,
76
+ retentionDays: options.retentionDays,
77
+ retentionStatuses: options.retentionStatuses,
78
+ cleanupIntervalMs: options.cleanupIntervalMs,
79
+ autoCleanup: options.autoCleanup,
80
+ cleanupOnStart: options.cleanupOnStart,
75
81
  });
76
82
 
77
83
  // Expose to ctx & app
@@ -90,6 +96,11 @@ function queuePlugin(options = {}) {
90
96
  this.queue = queueManager;
91
97
  },
92
98
 
99
+ api: {
100
+ getQueueManager: () => queueManager,
101
+ clean: (opts) => queueManager?.clean(opts),
102
+ },
103
+
93
104
  onRoutesReady(ctx) {
94
105
  if (jobsDir && fs.existsSync(jobsDir)) {
95
106
  queueManager.loadJobsFromDirectory(jobsDir);