webspresso 0.0.97 → 0.0.99
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 +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -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/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- 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/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- 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/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -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 +333 -36
- 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 +248 -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/client/verify-spa-parts.js +2 -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/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- 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/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -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/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -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 +4 -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/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
package/plugins/dashboard/app.js
CHANGED
|
@@ -10,6 +10,8 @@ const state = {
|
|
|
10
10
|
filter: 'all',
|
|
11
11
|
search: '',
|
|
12
12
|
routes: window.__DASHBOARD_DATA__.routes || [],
|
|
13
|
+
models: window.__DASHBOARD_DATA__.models || [],
|
|
14
|
+
services: window.__DASHBOARD_DATA__.services || [],
|
|
13
15
|
plugins: window.__DASHBOARD_DATA__.plugins || [],
|
|
14
16
|
config: window.__DASHBOARD_DATA__.config || {}
|
|
15
17
|
};
|
|
@@ -29,18 +31,27 @@ const Icons = {
|
|
|
29
31
|
// Components
|
|
30
32
|
const Header = {
|
|
31
33
|
view: () => m('.header', [
|
|
32
|
-
m('
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
m('div', [
|
|
35
|
+
m('h1', [
|
|
36
|
+
Icons.logo(),
|
|
37
|
+
'Webspresso Dashboard'
|
|
38
|
+
]),
|
|
39
|
+
m('span.dev-badge', 'development')
|
|
35
40
|
]),
|
|
36
|
-
m('
|
|
41
|
+
m('.header-actions', [
|
|
42
|
+
m('a.action-link', { href: '/_admin', target: '_blank', rel: 'noopener' }, '⚙️ Admin Panel ↗'),
|
|
43
|
+
m('a.action-link', { href: '/_swagger', target: '_blank', rel: 'noopener' }, '📖 API Docs ↗'),
|
|
44
|
+
m('a.action-link', { href: '/_schema', target: '_blank', rel: 'noopener' }, '🗂️ Schema Explorer ↗')
|
|
45
|
+
])
|
|
37
46
|
])
|
|
38
47
|
};
|
|
39
48
|
|
|
40
49
|
const Tabs = {
|
|
41
50
|
view: () => m('.tabs', [
|
|
42
|
-
['routes', 'Routes'],
|
|
43
|
-
['
|
|
51
|
+
['routes', 'Routes (' + state.routes.length + ')'],
|
|
52
|
+
['models', 'Models (' + state.models.length + ')'],
|
|
53
|
+
['services', 'Services (' + state.services.length + ')'],
|
|
54
|
+
['plugins', 'Plugins (' + state.plugins.length + ')'],
|
|
44
55
|
['config', 'Config']
|
|
45
56
|
].map(([id, label]) =>
|
|
46
57
|
m('button.tab', {
|
|
@@ -72,32 +83,36 @@ const FilterBar = {
|
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
const MethodBadge = {
|
|
75
|
-
view: (vnode) => m('span.method-badge.method-' + vnode.attrs.method,
|
|
76
|
-
vnode.attrs.method.toUpperCase()
|
|
86
|
+
view: (vnode) => m('span.method-badge.method-' + (vnode.attrs.method || 'get').toLowerCase(),
|
|
87
|
+
(vnode.attrs.method || 'GET').toUpperCase()
|
|
77
88
|
)
|
|
78
89
|
};
|
|
79
90
|
|
|
80
91
|
const TypeBadge = {
|
|
81
|
-
view: (vnode) =>
|
|
82
|
-
vnode.attrs.type
|
|
83
|
-
|
|
92
|
+
view: (vnode) => {
|
|
93
|
+
const rawType = vnode.attrs.type || 'page';
|
|
94
|
+
const label = rawType === 'page' ? 'SSR' : rawType.toUpperCase();
|
|
95
|
+
return m('span.type-badge.type-' + (rawType === 'page' ? 'ssr' : rawType), label);
|
|
96
|
+
}
|
|
84
97
|
};
|
|
85
98
|
|
|
86
99
|
const RoutesTable = {
|
|
87
100
|
view: () => {
|
|
88
101
|
let routes = state.routes;
|
|
89
102
|
|
|
90
|
-
// Apply filter
|
|
91
|
-
if (state.filter
|
|
92
|
-
routes = routes.filter(r => r.type ===
|
|
103
|
+
// Apply filter: SSR matches both 'ssr' and 'page'
|
|
104
|
+
if (state.filter === 'ssr') {
|
|
105
|
+
routes = routes.filter(r => r.type === 'ssr' || r.type === 'page');
|
|
106
|
+
} else if (state.filter === 'api') {
|
|
107
|
+
routes = routes.filter(r => r.type === 'api');
|
|
93
108
|
}
|
|
94
109
|
|
|
95
|
-
// Apply search
|
|
110
|
+
// Apply search: checks path or pattern or file
|
|
96
111
|
if (state.search) {
|
|
97
112
|
const search = state.search.toLowerCase();
|
|
98
113
|
routes = routes.filter(r =>
|
|
99
|
-
r.pattern.toLowerCase().includes(search) ||
|
|
100
|
-
r.file.toLowerCase().includes(search)
|
|
114
|
+
((r.path || r.pattern || '')).toLowerCase().includes(search) ||
|
|
115
|
+
((r.file || '')).toLowerCase().includes(search)
|
|
101
116
|
);
|
|
102
117
|
}
|
|
103
118
|
|
|
@@ -121,17 +136,18 @@ const RoutesTable = {
|
|
|
121
136
|
])
|
|
122
137
|
),
|
|
123
138
|
m('tbody',
|
|
124
|
-
routes.map(route =>
|
|
125
|
-
|
|
139
|
+
routes.map(route => {
|
|
140
|
+
const displayPath = route.path || route.pattern || '/';
|
|
141
|
+
return m('tr', [
|
|
126
142
|
m('td', m(MethodBadge, { method: route.method })),
|
|
127
143
|
m('td', [
|
|
128
|
-
m('span.code',
|
|
144
|
+
m('span.code', displayPath),
|
|
129
145
|
route.isDynamic ? m('span.dynamic-indicator', '⚡ dynamic') : null
|
|
130
146
|
]),
|
|
131
|
-
m('td', m('span.file-path', route.file)),
|
|
147
|
+
m('td', m('span.file-path', route.file || 'custom')),
|
|
132
148
|
m('td', m(TypeBadge, { type: route.type }))
|
|
133
|
-
])
|
|
134
|
-
)
|
|
149
|
+
]);
|
|
150
|
+
})
|
|
135
151
|
)
|
|
136
152
|
])
|
|
137
153
|
);
|
|
@@ -140,7 +156,7 @@ const RoutesTable = {
|
|
|
140
156
|
|
|
141
157
|
const StatsGrid = {
|
|
142
158
|
view: () => {
|
|
143
|
-
const ssrCount = state.routes.filter(r => r.type === 'ssr').length;
|
|
159
|
+
const ssrCount = state.routes.filter(r => r.type === 'ssr' || r.type === 'page').length;
|
|
144
160
|
const apiCount = state.routes.filter(r => r.type === 'api').length;
|
|
145
161
|
const dynamicCount = state.routes.filter(r => r.isDynamic).length;
|
|
146
162
|
|
|
@@ -173,6 +189,116 @@ const RoutesView = {
|
|
|
173
189
|
])
|
|
174
190
|
};
|
|
175
191
|
|
|
192
|
+
const ModelsView = {
|
|
193
|
+
view: () => {
|
|
194
|
+
const models = state.models || [];
|
|
195
|
+
if (models.length === 0) {
|
|
196
|
+
return m('.table-container',
|
|
197
|
+
m('.empty-state', [
|
|
198
|
+
Icons.empty(),
|
|
199
|
+
m('p', 'No ORM models registered in core/orm')
|
|
200
|
+
])
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return m('div', [
|
|
205
|
+
m('.stats-grid', [
|
|
206
|
+
m('.card', [
|
|
207
|
+
m('.card-title', 'Total Models'),
|
|
208
|
+
m('.card-value', models.length)
|
|
209
|
+
]),
|
|
210
|
+
m('.card', [
|
|
211
|
+
m('.card-title', 'Cached Models'),
|
|
212
|
+
m('.card-value', models.filter(m => Boolean(m.cache)).length)
|
|
213
|
+
]),
|
|
214
|
+
m('.card', [
|
|
215
|
+
m('.card-title', 'Soft-Delete Enabled'),
|
|
216
|
+
m('.card-value', models.filter(m => Boolean(m.softDelete)).length)
|
|
217
|
+
])
|
|
218
|
+
]),
|
|
219
|
+
m('.table-container',
|
|
220
|
+
m('table', [
|
|
221
|
+
m('thead',
|
|
222
|
+
m('tr', [
|
|
223
|
+
m('th', 'Model'),
|
|
224
|
+
m('th', 'Table'),
|
|
225
|
+
m('th', 'Primary Key'),
|
|
226
|
+
m('th', 'Columns'),
|
|
227
|
+
m('th', 'Soft Delete'),
|
|
228
|
+
m('th', 'Cache')
|
|
229
|
+
])
|
|
230
|
+
),
|
|
231
|
+
m('tbody',
|
|
232
|
+
models.map(mod =>
|
|
233
|
+
m('tr', [
|
|
234
|
+
m('td', m('strong', mod.name)),
|
|
235
|
+
m('td', m('span.code-badge', mod.table)),
|
|
236
|
+
m('td', m('span.badge.badge-blue', mod.primaryKey || 'id')),
|
|
237
|
+
m('td', (mod.columnCount || 0) + ' columns'),
|
|
238
|
+
m('td', mod.softDelete ? m('span.badge.badge-green', 'Enabled') : m('span.badge.badge-gray', 'Disabled')),
|
|
239
|
+
m('td', mod.cache ? m('span.badge.badge-purple', String(mod.cache)) : m('span.badge.badge-gray', 'None'))
|
|
240
|
+
])
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
])
|
|
244
|
+
)
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const ServicesView = {
|
|
250
|
+
view: () => {
|
|
251
|
+
const services = state.services || [];
|
|
252
|
+
if (services.length === 0) {
|
|
253
|
+
return m('.table-container',
|
|
254
|
+
m('.empty-state', [
|
|
255
|
+
Icons.empty(),
|
|
256
|
+
m('p', 'No services registered in services/ or ServiceRegistry')
|
|
257
|
+
])
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return m('div', [
|
|
262
|
+
m('.stats-grid', [
|
|
263
|
+
m('.card', [
|
|
264
|
+
m('.card-title', 'Total Services'),
|
|
265
|
+
m('.card-value', services.length)
|
|
266
|
+
]),
|
|
267
|
+
m('.card', [
|
|
268
|
+
m('.card-title', 'ACID Transactions'),
|
|
269
|
+
m('.card-value', services.filter(s => s.transaction).length)
|
|
270
|
+
]),
|
|
271
|
+
m('.card', [
|
|
272
|
+
m('.card-title', 'Auth Protected'),
|
|
273
|
+
m('.card-value', services.filter(s => s.auth).length)
|
|
274
|
+
])
|
|
275
|
+
]),
|
|
276
|
+
m('.table-container',
|
|
277
|
+
m('table', [
|
|
278
|
+
m('thead',
|
|
279
|
+
m('tr', [
|
|
280
|
+
m('th', 'Service Name'),
|
|
281
|
+
m('th', 'Transaction (ACID)'),
|
|
282
|
+
m('th', 'Auth Guard'),
|
|
283
|
+
m('th', 'Schema Validation')
|
|
284
|
+
])
|
|
285
|
+
),
|
|
286
|
+
m('tbody',
|
|
287
|
+
services.map(srv =>
|
|
288
|
+
m('tr', [
|
|
289
|
+
m('td', m('span.code-badge', srv.name)),
|
|
290
|
+
m('td', srv.transaction ? m('span.badge.badge-green', 'Active') : m('span.badge.badge-gray', 'No')),
|
|
291
|
+
m('td', srv.auth ? m('span.badge.badge-orange', String(srv.auth)) : m('span.badge.badge-gray', 'Public')),
|
|
292
|
+
m('td', srv.hasSchema ? m('span.badge.badge-blue', 'Zod Schema') : m('span.badge.badge-gray', 'None'))
|
|
293
|
+
])
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
])
|
|
297
|
+
)
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
176
302
|
const PluginsView = {
|
|
177
303
|
view: () => {
|
|
178
304
|
if (state.plugins.length === 0) {
|
|
@@ -254,6 +380,8 @@ const App = {
|
|
|
254
380
|
m(Header),
|
|
255
381
|
m(Tabs),
|
|
256
382
|
state.activeTab === 'routes' ? m(RoutesView) : null,
|
|
383
|
+
state.activeTab === 'models' ? m(ModelsView) : null,
|
|
384
|
+
state.activeTab === 'services' ? m(ServicesView) : null,
|
|
257
385
|
state.activeTab === 'plugins' ? m(PluginsView) : null,
|
|
258
386
|
state.activeTab === 'config' ? m(ConfigView) : null
|
|
259
387
|
])
|
|
@@ -262,6 +390,3 @@ const App = {
|
|
|
262
390
|
// Mount app
|
|
263
391
|
m.mount(document.getElementById('app'), App);
|
|
264
392
|
`;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
@@ -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
|
});
|