webspresso 0.0.97 → 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.
- package/.agents/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/index.js +4 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +210 -62
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
|
@@ -1,29 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Webspresso Dashboard Plugin
|
|
3
|
-
* Development dashboard for monitoring routes, plugins, and configuration
|
|
5
|
+
* Development dashboard for monitoring routes, ORM models, services, plugins, and configuration
|
|
4
6
|
* Uses Mithril.js for the SPA interface
|
|
7
|
+
* @module plugins/dashboard
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
const styles = require('./styles');
|
|
8
11
|
const appScript = require('./app');
|
|
12
|
+
const { getAllModels } = require('../../core/orm/model');
|
|
13
|
+
const { getServiceRegistry, hasServiceRegistry, hasDb, getDb } = require('../../src/app-context');
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Filter sensitive environment variables
|
|
17
|
+
* @param {Object} env
|
|
18
|
+
* @returns {Object}
|
|
12
19
|
*/
|
|
13
20
|
function filterSensitiveEnv(env) {
|
|
14
21
|
const sensitiveKeys = ['SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL', 'PRIVATE'];
|
|
15
22
|
const filtered = {};
|
|
16
|
-
|
|
23
|
+
|
|
17
24
|
for (const [key, value] of Object.entries(env)) {
|
|
18
|
-
const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
|
|
25
|
+
const isSensitive = sensitiveKeys.some((s) => key.toUpperCase().includes(s));
|
|
19
26
|
filtered[key] = isSensitive ? '••••••••' : value;
|
|
20
27
|
}
|
|
21
|
-
|
|
28
|
+
|
|
22
29
|
return filtered;
|
|
23
30
|
}
|
|
24
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Safely collects all registered ORM models
|
|
34
|
+
* @returns {Array<Object>}
|
|
35
|
+
*/
|
|
36
|
+
function collectModels() {
|
|
37
|
+
try {
|
|
38
|
+
const models = getAllModels();
|
|
39
|
+
const result = [];
|
|
40
|
+
for (const [name, model] of models.entries()) {
|
|
41
|
+
result.push({
|
|
42
|
+
name,
|
|
43
|
+
table: model.table || name.toLowerCase(),
|
|
44
|
+
primaryKey: model.primaryKey || 'id',
|
|
45
|
+
columns: model.schema && model.schema.shape ? Object.keys(model.schema.shape) : [],
|
|
46
|
+
columnCount: model.schema && model.schema.shape ? Object.keys(model.schema.shape).length : 0,
|
|
47
|
+
relations: Object.keys(model.relations || {}),
|
|
48
|
+
scopes: Object.keys(model.scopes || {}),
|
|
49
|
+
softDelete: Boolean(model.scopes?.softDelete || model.scopes?.notDeleted || model.softDelete || model.paranoid),
|
|
50
|
+
cache: model.cache || false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Safely collects all registered business services
|
|
61
|
+
* @returns {Array<Object>}
|
|
62
|
+
*/
|
|
63
|
+
function collectServices() {
|
|
64
|
+
try {
|
|
65
|
+
if (!hasServiceRegistry()) return [];
|
|
66
|
+
const registry = getServiceRegistry();
|
|
67
|
+
const result = [];
|
|
68
|
+
if (registry && registry.services) {
|
|
69
|
+
for (const [name, def] of registry.services.entries()) {
|
|
70
|
+
result.push({
|
|
71
|
+
name,
|
|
72
|
+
transaction: Boolean(def.transaction),
|
|
73
|
+
auth: def.auth !== undefined ? def.auth : false,
|
|
74
|
+
timeout: def.timeout || null,
|
|
75
|
+
hasSchema: Boolean(def.schema),
|
|
76
|
+
cache: def.cache ? true : false,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
25
86
|
/**
|
|
26
87
|
* Generate the dashboard HTML
|
|
88
|
+
* @param {Object} data
|
|
89
|
+
* @returns {string}
|
|
27
90
|
*/
|
|
28
91
|
function generateDashboardHtml(data) {
|
|
29
92
|
return `<!DOCTYPE html>
|
|
@@ -48,25 +111,25 @@ function generateDashboardHtml(data) {
|
|
|
48
111
|
/**
|
|
49
112
|
* Create the dashboard plugin
|
|
50
113
|
* @param {Object} options - Plugin options
|
|
51
|
-
* @param {string} options.path - Dashboard path
|
|
52
|
-
* @param {boolean} options.enabled - Force enable/disable (default: auto based on NODE_ENV)
|
|
114
|
+
* @param {string} [options.path='/_webspresso'] - Dashboard path
|
|
115
|
+
* @param {boolean} [options.enabled] - Force enable/disable (default: auto based on NODE_ENV)
|
|
53
116
|
*/
|
|
54
117
|
function dashboardPlugin(options = {}) {
|
|
55
118
|
const {
|
|
56
119
|
path: dashboardPath = '/_webspresso',
|
|
57
|
-
enabled
|
|
120
|
+
enabled,
|
|
58
121
|
} = options;
|
|
59
|
-
|
|
122
|
+
|
|
60
123
|
// Determine if enabled (default: only in development)
|
|
61
|
-
const isEnabled = enabled !== undefined
|
|
62
|
-
? enabled
|
|
124
|
+
const isEnabled = enabled !== undefined
|
|
125
|
+
? enabled
|
|
63
126
|
: process.env.NODE_ENV !== 'production';
|
|
64
|
-
|
|
127
|
+
|
|
65
128
|
return {
|
|
66
129
|
name: 'dashboard',
|
|
67
130
|
version: '1.0.0',
|
|
68
|
-
description: 'Development dashboard for monitoring routes and configuration',
|
|
69
|
-
|
|
131
|
+
description: 'Development dashboard for monitoring routes, models, services, and configuration',
|
|
132
|
+
|
|
70
133
|
/**
|
|
71
134
|
* Called after routes are mounted
|
|
72
135
|
*/
|
|
@@ -75,68 +138,109 @@ function dashboardPlugin(options = {}) {
|
|
|
75
138
|
if (!isEnabled) {
|
|
76
139
|
return;
|
|
77
140
|
}
|
|
78
|
-
|
|
79
|
-
const { app, routes, options: appOptions } = ctx;
|
|
80
|
-
|
|
141
|
+
|
|
142
|
+
const { app, routes = [], options: appOptions = {} } = ctx;
|
|
143
|
+
|
|
81
144
|
// Collect plugin info
|
|
82
|
-
const pluginManager = ctx.pluginManager ||
|
|
145
|
+
const pluginManager = ctx.pluginManager || appOptions.pluginManager;
|
|
83
146
|
const plugins = [];
|
|
84
|
-
|
|
147
|
+
|
|
85
148
|
// Get plugins from plugin manager if available
|
|
86
149
|
if (pluginManager && pluginManager.plugins) {
|
|
87
150
|
for (const [name, plugin] of pluginManager.plugins) {
|
|
88
151
|
plugins.push({
|
|
89
152
|
name: plugin.name || name,
|
|
90
153
|
version: plugin.version || '0.0.0',
|
|
91
|
-
description: plugin.description || ''
|
|
154
|
+
description: plugin.description || '',
|
|
92
155
|
});
|
|
93
156
|
}
|
|
94
157
|
}
|
|
95
|
-
|
|
158
|
+
|
|
159
|
+
// Collect models and services
|
|
160
|
+
const models = collectModels();
|
|
161
|
+
const services = collectServices();
|
|
162
|
+
|
|
96
163
|
// Build config data
|
|
97
164
|
const config = {
|
|
98
165
|
env: filterSensitiveEnv({
|
|
99
166
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
100
167
|
PORT: process.env.PORT || '3000',
|
|
101
|
-
BASE_URL: process.env.BASE_URL || 'http://localhost:3000'
|
|
168
|
+
BASE_URL: process.env.BASE_URL || 'http://localhost:3000',
|
|
102
169
|
}),
|
|
103
170
|
i18n: {
|
|
104
171
|
defaultLocale: process.env.DEFAULT_LOCALE || 'en',
|
|
105
|
-
supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(',')
|
|
172
|
+
supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(','),
|
|
106
173
|
},
|
|
107
174
|
server: {
|
|
108
175
|
port: process.env.PORT || '3000',
|
|
109
|
-
baseUrl: process.env.BASE_URL || 'http://localhost:3000'
|
|
110
|
-
}
|
|
176
|
+
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
|
|
177
|
+
},
|
|
178
|
+
database: {
|
|
179
|
+
connected: hasDb(),
|
|
180
|
+
dialect: hasDb() ? (getDb().client || getDb().knex?.client?.config?.client || 'knex') : 'none',
|
|
181
|
+
},
|
|
111
182
|
};
|
|
112
|
-
|
|
113
|
-
|
|
183
|
+
|
|
184
|
+
const overview = {
|
|
185
|
+
routesCount: routes.length,
|
|
186
|
+
modelsCount: models.length,
|
|
187
|
+
servicesCount: services.length,
|
|
188
|
+
pluginsCount: plugins.length,
|
|
189
|
+
uptime: process.uptime(),
|
|
190
|
+
memoryUsage: process.memoryUsage(),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// 1. Dashboard HTML endpoint
|
|
114
194
|
ctx.addRoute('get', dashboardPath, (req, res) => {
|
|
115
|
-
const data = { routes, plugins, config };
|
|
195
|
+
const data = { routes, plugins, config, models, services, overview };
|
|
116
196
|
res.type('text/html');
|
|
117
197
|
res.send(generateDashboardHtml(data));
|
|
118
198
|
});
|
|
119
|
-
|
|
120
|
-
// JSON API endpoints
|
|
121
|
-
ctx.addRoute('get', dashboardPath
|
|
199
|
+
|
|
200
|
+
// 2. JSON API endpoints
|
|
201
|
+
ctx.addRoute('get', `${dashboardPath}/api/routes`, (req, res) => {
|
|
122
202
|
res.json(routes);
|
|
123
203
|
});
|
|
124
|
-
|
|
125
|
-
ctx.addRoute('get', dashboardPath
|
|
204
|
+
|
|
205
|
+
ctx.addRoute('get', `${dashboardPath}/api/plugins`, (req, res) => {
|
|
126
206
|
res.json(plugins);
|
|
127
207
|
});
|
|
128
|
-
|
|
129
|
-
ctx.addRoute('get', dashboardPath
|
|
208
|
+
|
|
209
|
+
ctx.addRoute('get', `${dashboardPath}/api/config`, (req, res) => {
|
|
130
210
|
res.json(config);
|
|
131
211
|
});
|
|
132
|
-
|
|
212
|
+
|
|
213
|
+
ctx.addRoute('get', `${dashboardPath}/api/models`, (req, res) => {
|
|
214
|
+
res.json(collectModels());
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
ctx.addRoute('get', `${dashboardPath}/api/services`, (req, res) => {
|
|
218
|
+
res.json(collectServices());
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
ctx.addRoute('get', `${dashboardPath}/api/overview`, (req, res) => {
|
|
222
|
+
res.json({
|
|
223
|
+
routesCount: (ctx.routes || []).length,
|
|
224
|
+
modelsCount: collectModels().length,
|
|
225
|
+
servicesCount: collectServices().length,
|
|
226
|
+
pluginsCount: plugins.length,
|
|
227
|
+
nodeVersion: process.version,
|
|
228
|
+
platform: process.platform,
|
|
229
|
+
uptime: process.uptime(),
|
|
230
|
+
memoryUsage: process.memoryUsage(),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
133
234
|
// Log dashboard URL
|
|
134
235
|
console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 3000}${dashboardPath}\n`);
|
|
135
|
-
}
|
|
236
|
+
},
|
|
136
237
|
};
|
|
137
238
|
}
|
|
138
239
|
|
|
139
240
|
module.exports = dashboardPlugin;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
241
|
+
module.exports.dashboardPlugin = dashboardPlugin;
|
|
242
|
+
module.exports.devToolbarPlugin = dashboardPlugin;
|
|
243
|
+
module.exports.devtoolsPlugin = dashboardPlugin;
|
|
244
|
+
module.exports.generateDashboardHtml = generateDashboardHtml;
|
|
245
|
+
module.exports.collectModels = collectModels;
|
|
246
|
+
module.exports.collectServices = collectServices;
|
|
@@ -378,6 +378,84 @@ tr:hover td {
|
|
|
378
378
|
animation: spin 0.8s linear infinite;
|
|
379
379
|
margin-right: 12px;
|
|
380
380
|
}
|
|
381
|
+
|
|
382
|
+
/* Header Actions */
|
|
383
|
+
.header-actions {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 8px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.action-link {
|
|
390
|
+
display: inline-flex;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 6px;
|
|
393
|
+
padding: 6px 12px;
|
|
394
|
+
border-radius: 6px;
|
|
395
|
+
font-size: 13px;
|
|
396
|
+
font-weight: 500;
|
|
397
|
+
text-decoration: none;
|
|
398
|
+
background: var(--bg-tertiary);
|
|
399
|
+
border: 1px solid var(--border);
|
|
400
|
+
color: var(--text-primary);
|
|
401
|
+
transition: all 0.15s ease;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.action-link:hover {
|
|
405
|
+
background: var(--border);
|
|
406
|
+
border-color: var(--accent);
|
|
407
|
+
color: var(--text-primary);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Badges */
|
|
411
|
+
.badge {
|
|
412
|
+
display: inline-block;
|
|
413
|
+
padding: 2px 8px;
|
|
414
|
+
border-radius: 10px;
|
|
415
|
+
font-size: 11px;
|
|
416
|
+
font-weight: 600;
|
|
417
|
+
text-transform: uppercase;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.badge-green {
|
|
421
|
+
background: rgba(63, 185, 80, 0.15);
|
|
422
|
+
color: var(--green);
|
|
423
|
+
border: 1px solid rgba(63, 185, 80, 0.3);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.badge-blue {
|
|
427
|
+
background: rgba(88, 166, 255, 0.15);
|
|
428
|
+
color: var(--accent);
|
|
429
|
+
border: 1px solid rgba(88, 166, 255, 0.3);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.badge-purple {
|
|
433
|
+
background: rgba(163, 113, 247, 0.15);
|
|
434
|
+
color: var(--purple);
|
|
435
|
+
border: 1px solid rgba(163, 113, 247, 0.3);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.badge-orange {
|
|
439
|
+
background: rgba(210, 153, 34, 0.15);
|
|
440
|
+
color: var(--orange);
|
|
441
|
+
border: 1px solid rgba(210, 153, 34, 0.3);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.badge-gray {
|
|
445
|
+
background: rgba(110, 118, 129, 0.15);
|
|
446
|
+
color: var(--text-muted);
|
|
447
|
+
border: 1px solid rgba(110, 118, 129, 0.3);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.code-badge {
|
|
451
|
+
font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace;
|
|
452
|
+
font-size: 12px;
|
|
453
|
+
background: var(--bg-tertiary);
|
|
454
|
+
padding: 2px 6px;
|
|
455
|
+
border-radius: 4px;
|
|
456
|
+
border: 1px solid var(--border);
|
|
457
|
+
color: var(--accent);
|
|
458
|
+
}
|
|
381
459
|
`;
|
|
382
460
|
|
|
383
461
|
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
* @module plugins/data-exchange/export-xlsx
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let ExcelJS = null;
|
|
7
|
+
function getExcelJS() {
|
|
8
|
+
if (!ExcelJS) {
|
|
9
|
+
ExcelJS = require('exceljs');
|
|
10
|
+
}
|
|
11
|
+
return ExcelJS;
|
|
12
|
+
}
|
|
7
13
|
const { sanitizeForOutput } = require('../../core/orm/utils');
|
|
8
14
|
const { resolveExportRecords } = require('./record-selection');
|
|
9
15
|
|
|
@@ -42,7 +48,8 @@ async function buildXlsxBuffer(model, records) {
|
|
|
42
48
|
const clean = sanitizeForOutput(records, model);
|
|
43
49
|
const columns = getModelExportColumns(model, clean);
|
|
44
50
|
|
|
45
|
-
const
|
|
51
|
+
const ExcelLib = getExcelJS();
|
|
52
|
+
const wb = new ExcelLib.Workbook();
|
|
46
53
|
const ws = wb.addWorksheet('Export', {
|
|
47
54
|
views: [{ state: 'frozen', ySplit: 1 }],
|
|
48
55
|
});
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const multer = require('multer');
|
|
7
|
-
|
|
7
|
+
let ExcelJS = null;
|
|
8
|
+
function getExcelJS() {
|
|
9
|
+
if (!ExcelJS) {
|
|
10
|
+
ExcelJS = require('exceljs');
|
|
11
|
+
}
|
|
12
|
+
return ExcelJS;
|
|
13
|
+
}
|
|
8
14
|
const { parse: parseCsv } = require('csv-parse/sync');
|
|
9
15
|
const { buildHeaderMapping, dataRowsToObjects } = require('./parse-table');
|
|
10
16
|
|
|
@@ -132,7 +138,8 @@ function parseCsvToRows(buffer) {
|
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
async function parseXlsxToRows(buffer) {
|
|
135
|
-
const
|
|
141
|
+
const ExcelLib = getExcelJS();
|
|
142
|
+
const wb = new ExcelLib.Workbook();
|
|
136
143
|
await wb.xlsx.load(buffer);
|
|
137
144
|
const ws = wb.worksheets[0];
|
|
138
145
|
if (!ws) return [];
|
|
@@ -76,14 +76,17 @@ function dataExchangePlugin(options = {}) {
|
|
|
76
76
|
models: '*',
|
|
77
77
|
confirm: false,
|
|
78
78
|
handler: async (records, modelName, { db: d }) => {
|
|
79
|
+
const targetModel = typeof modelName === 'object' && modelName !== null
|
|
80
|
+
? (modelName.name || modelName.modelName || modelName.model || String(modelName))
|
|
81
|
+
: String(modelName);
|
|
79
82
|
const { getModel } = require('../../core/orm/model');
|
|
80
|
-
const model = d.getModel ? d.getModel(
|
|
83
|
+
const model = d.getModel ? d.getModel(targetModel) : getModel(targetModel);
|
|
81
84
|
const pk = model?.primaryKey || 'id';
|
|
82
85
|
const ids = records.map((r) => r[pk]).join(',');
|
|
83
86
|
return {
|
|
84
87
|
download: true,
|
|
85
|
-
url: `${adminPath}/api/data-exchange/export/${
|
|
86
|
-
filename: `${
|
|
88
|
+
url: `${adminPath}/api/data-exchange/export/${targetModel}?ids=${encodeURIComponent(ids)}`,
|
|
89
|
+
filename: `${targetModel}_export.xlsx`,
|
|
87
90
|
};
|
|
88
91
|
},
|
|
89
92
|
});
|
package/plugins/email/index.js
CHANGED
|
@@ -29,10 +29,21 @@ const { emailPluginOptionsFromManifest } = require('./manifest');
|
|
|
29
29
|
* @param {Object} [options.authEmails] - Auth email bridge config
|
|
30
30
|
* @param {Object} [options.emailTemplates] - Precompiled templates from build manifest
|
|
31
31
|
* @param {Object} [options.manifest] - Full build manifest (uses emailTemplates field)
|
|
32
|
+
* @param {Object|boolean} [options.retention] - Retention policy configuration (set false to disable)
|
|
33
|
+
* @param {number} [options.retention.days=90] - Days to retain email logs
|
|
34
|
+
* @param {number} [options.retention.intervalMs=86400000] - Auto-cleanup timer frequency (24h)
|
|
35
|
+
* @param {boolean} [options.retention.runOnStart=true] - Run initial purge shortly after startup
|
|
36
|
+
* @param {number} [options.retentionDays=90] - Shorthand for retention.days
|
|
37
|
+
* @param {number} [options.cleanupIntervalMs=86400000] - Shorthand for retention.intervalMs
|
|
38
|
+
* @param {boolean} [options.autoCleanup=true] - Enable automatic background cleanup
|
|
39
|
+
* @param {boolean} [options.cleanupOnStart=true] - Run initial purge on start
|
|
40
|
+
* @param {number} [options.cleanupStartDelayMs] - Delay in ms before start purge
|
|
32
41
|
*/
|
|
33
42
|
function emailPlugin(options = {}) {
|
|
34
43
|
const {
|
|
35
44
|
db,
|
|
45
|
+
title,
|
|
46
|
+
adminTitle,
|
|
36
47
|
transport,
|
|
37
48
|
smtp,
|
|
38
49
|
defaults = {},
|
|
@@ -48,6 +59,8 @@ function emailPlugin(options = {}) {
|
|
|
48
59
|
manifest,
|
|
49
60
|
} = options;
|
|
50
61
|
|
|
62
|
+
const pageTitle = title || adminTitle || 'Email';
|
|
63
|
+
|
|
51
64
|
const logToDb = options.logToDb !== undefined ? options.logToDb : !!db;
|
|
52
65
|
const knex = db ? (db.knex || db) : null;
|
|
53
66
|
|
|
@@ -55,6 +68,90 @@ function emailPlugin(options = {}) {
|
|
|
55
68
|
console.warn('[email] logToDb is enabled but no db was passed — logging disabled');
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
// Retention configuration
|
|
72
|
+
const retentionOption = options.retention;
|
|
73
|
+
const isRetentionDisabled =
|
|
74
|
+
retentionOption === false ||
|
|
75
|
+
(typeof retentionOption === 'object' && retentionOption.enabled === false) ||
|
|
76
|
+
options.retentionDays === null ||
|
|
77
|
+
options.retentionDays === 0 ||
|
|
78
|
+
!logToDb ||
|
|
79
|
+
!knex;
|
|
80
|
+
|
|
81
|
+
const autoCleanupEnabled =
|
|
82
|
+
!isRetentionDisabled &&
|
|
83
|
+
options.autoCleanup !== false &&
|
|
84
|
+
(typeof retentionOption !== 'object' || retentionOption.autoCleanup !== false);
|
|
85
|
+
|
|
86
|
+
const retentionConfig = {
|
|
87
|
+
enabled: !isRetentionDisabled,
|
|
88
|
+
autoCleanup: autoCleanupEnabled,
|
|
89
|
+
days: (typeof options.retentionDays === 'number' && options.retentionDays > 0)
|
|
90
|
+
? options.retentionDays
|
|
91
|
+
: (typeof retentionOption === 'object' && retentionOption?.days ? retentionOption.days : 90),
|
|
92
|
+
intervalMs: (typeof options.cleanupIntervalMs === 'number' && options.cleanupIntervalMs > 0)
|
|
93
|
+
? options.cleanupIntervalMs
|
|
94
|
+
: (typeof retentionOption === 'object' && retentionOption?.intervalMs ? retentionOption.intervalMs : 24 * 60 * 60 * 1000),
|
|
95
|
+
runOnStart: (typeof retentionOption === 'object' && retentionOption?.runOnStart !== undefined)
|
|
96
|
+
? !!retentionOption.runOnStart
|
|
97
|
+
: (options.cleanupOnStart !== undefined ? !!options.cleanupOnStart : true),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let cleanupInterval = null;
|
|
101
|
+
let cleanupStartTimeout = null;
|
|
102
|
+
|
|
103
|
+
async function runCleanup() {
|
|
104
|
+
if (!knex) return 0;
|
|
105
|
+
try {
|
|
106
|
+
return await purgeEmailLogs(knex, {
|
|
107
|
+
tableName,
|
|
108
|
+
days: retentionConfig.days,
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
112
|
+
console.warn('[email] Failed to run retention cleanup:', err.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function startCleanup(customIntervalMs) {
|
|
118
|
+
stopCleanup();
|
|
119
|
+
if (!retentionConfig.enabled || !knex) return;
|
|
120
|
+
if (typeof customIntervalMs === 'number' && customIntervalMs > 0) {
|
|
121
|
+
retentionConfig.intervalMs = customIntervalMs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (retentionConfig.runOnStart) {
|
|
125
|
+
const delayMs = typeof options.cleanupStartDelayMs === 'number'
|
|
126
|
+
? options.cleanupStartDelayMs
|
|
127
|
+
: (process.env.NODE_ENV === 'test' ? 50 : 5000);
|
|
128
|
+
cleanupStartTimeout = setTimeout(() => {
|
|
129
|
+
runCleanup();
|
|
130
|
+
}, delayMs);
|
|
131
|
+
if (cleanupStartTimeout && cleanupStartTimeout.unref) {
|
|
132
|
+
cleanupStartTimeout.unref();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
cleanupInterval = setInterval(() => {
|
|
137
|
+
runCleanup();
|
|
138
|
+
}, retentionConfig.intervalMs);
|
|
139
|
+
if (cleanupInterval && cleanupInterval.unref) {
|
|
140
|
+
cleanupInterval.unref();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stopCleanup() {
|
|
145
|
+
if (cleanupStartTimeout) {
|
|
146
|
+
clearTimeout(cleanupStartTimeout);
|
|
147
|
+
cleanupStartTimeout = null;
|
|
148
|
+
}
|
|
149
|
+
if (cleanupInterval) {
|
|
150
|
+
clearInterval(cleanupInterval);
|
|
151
|
+
cleanupInterval = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
58
155
|
const registry = new TemplateRegistry();
|
|
59
156
|
|
|
60
157
|
const manifestChunks = emailTemplatesOpt
|
|
@@ -81,7 +178,7 @@ function emailPlugin(options = {}) {
|
|
|
81
178
|
return {
|
|
82
179
|
name: 'email',
|
|
83
180
|
version: '1.0.0',
|
|
84
|
-
description: 'MJML + Nodemailer email sending with optional admin UI and
|
|
181
|
+
description: 'MJML + Nodemailer email sending with optional admin UI, auth integration, and retention cleanup',
|
|
85
182
|
dependencies: includeAdminPage ? { 'admin-panel': '*' } : {},
|
|
86
183
|
|
|
87
184
|
api: {
|
|
@@ -92,13 +189,29 @@ function emailPlugin(options = {}) {
|
|
|
92
189
|
listTemplates: () => registry.list(),
|
|
93
190
|
verifyConnection: () => emailService.verifyConnection(),
|
|
94
191
|
queryLogs: (filters) => emailService.queryLogs(filters),
|
|
192
|
+
purge: (opts = {}) => {
|
|
193
|
+
if (!knex) throw new Error('Database is required for purge');
|
|
194
|
+
return purgeEmailLogs(knex, {
|
|
195
|
+
tableName: opts.tableName || tableName,
|
|
196
|
+
days: opts.days !== undefined ? opts.days : (opts.retentionDays !== undefined ? opts.retentionDays : retentionConfig.days),
|
|
197
|
+
olderThan: opts.olderThan,
|
|
198
|
+
...opts,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
95
201
|
purgeLogs: (opts = {}) => {
|
|
96
202
|
if (!knex) throw new Error('Database is required for purgeLogs');
|
|
97
203
|
return purgeEmailLogs(knex, {
|
|
98
204
|
tableName: opts.tableName || tableName,
|
|
99
205
|
olderThan: opts.olderThan,
|
|
206
|
+
days: opts.days !== undefined ? opts.days : (opts.retentionDays !== undefined ? opts.retentionDays : retentionConfig.days),
|
|
207
|
+
...opts,
|
|
100
208
|
});
|
|
101
209
|
},
|
|
210
|
+
startCleanup,
|
|
211
|
+
stopCleanup,
|
|
212
|
+
getRetentionConfig() {
|
|
213
|
+
return { ...retentionConfig };
|
|
214
|
+
},
|
|
102
215
|
getMigrationTemplate: (name) => generateEmailLogsMigration(name || tableName),
|
|
103
216
|
isTransportReady: () => emailService.isTransportReady(),
|
|
104
217
|
loadFromManifest: (chunks) => registry.loadFromManifest(chunks),
|
|
@@ -120,6 +233,22 @@ function emailPlugin(options = {}) {
|
|
|
120
233
|
}
|
|
121
234
|
}
|
|
122
235
|
}
|
|
236
|
+
|
|
237
|
+
if (retentionConfig.enabled && autoCleanupEnabled) {
|
|
238
|
+
startCleanup();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return () => {
|
|
242
|
+
stopCleanup();
|
|
243
|
+
};
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
dispose() {
|
|
247
|
+
stopCleanup();
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
shutdown() {
|
|
251
|
+
stopCleanup();
|
|
123
252
|
},
|
|
124
253
|
|
|
125
254
|
onRoutesReady(ctx) {
|
|
@@ -159,10 +288,11 @@ function emailPlugin(options = {}) {
|
|
|
159
288
|
|
|
160
289
|
adminApi.registerModule({
|
|
161
290
|
id: 'email',
|
|
291
|
+
title: pageTitle,
|
|
162
292
|
|
|
163
293
|
pages: [{
|
|
164
294
|
id: 'email',
|
|
165
|
-
title:
|
|
295
|
+
title: pageTitle,
|
|
166
296
|
path: '/email',
|
|
167
297
|
icon: 'mail',
|
|
168
298
|
component: generateEmailComponent({ apiPrefix }),
|