webspresso 0.0.45 → 0.0.47

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/README.md CHANGED
@@ -254,6 +254,7 @@ Creates and configures the Express app.
254
254
  - `pagesDir` (required): Path to pages directory
255
255
  - `viewsDir` (optional): Path to views/layouts directory
256
256
  - `publicDir` (optional): Path to public/static directory
257
+ - `db` (optional): Database instance — exposed as `ctx.db` in plugin hooks (`register`, `onRoutesReady`) and in page `load`/`meta` functions
257
258
  - `logging` (optional): Enable request logging (default: true in development)
258
259
  - `helmet` (optional): Helmet security configuration
259
260
  - `true` or `undefined`: Use default secure configuration
@@ -633,6 +634,11 @@ const myPlugin = {
633
634
  // Access Express app
634
635
  ctx.app.use((req, res, next) => next());
635
636
 
637
+ // Access database (when createApp({ db }) is used)
638
+ if (ctx.db) {
639
+ // Use ctx.db.getRepository('Model'), ctx.db.knex, etc.
640
+ }
641
+
636
642
  // Add template helpers
637
643
  ctx.addHelper('myHelper', () => 'Hello!');
638
644
 
@@ -645,6 +651,7 @@ const myPlugin = {
645
651
 
646
652
  // Called after all routes are mounted
647
653
  onRoutesReady(ctx) {
654
+ // ctx.db available when createApp({ db }) is used
648
655
  // Access route metadata
649
656
  console.log('Routes:', ctx.routes);
650
657
 
@@ -777,6 +784,11 @@ module.exports = {
777
784
 
778
785
  // Load data for SSR
779
786
  async load(req, ctx) {
787
+ // ctx.db is available when createApp({ db }) is used
788
+ if (ctx.db) {
789
+ const posts = await ctx.db.getRepository('Post').query().limit(10);
790
+ return { posts };
791
+ }
780
792
  return { tools: await fetchTools() };
781
793
  },
782
794
 
@@ -1043,6 +1055,8 @@ const db = createDatabase({
1043
1055
  const UserRepo = db.getRepository('User');
1044
1056
  ```
1045
1057
 
1058
+ Pass `db` to `createApp({ db })` to expose it as `ctx.db` in plugin hooks and page `load`/`meta` functions.
1059
+
1046
1060
  **Model File Structure:**
1047
1061
  - Place model files in `models/` directory (or custom path via `config.models`)
1048
1062
  - Each file should export a model defined with `defineModel()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Admin Module Registration System
3
+ * Declarative API for registering admin pages, menu items, API routes, and widgets
4
+ * @module plugins/admin-panel/core/admin-module
5
+ */
6
+
7
+ /**
8
+ * Register an admin module with all its components in a single declarative call
9
+ * @param {Object} config - Module configuration
10
+ * @param {string} config.id - Unique module identifier (required)
11
+ * @param {Array} [config.pages] - Custom admin pages
12
+ * @param {Array} [config.menu] - Sidebar menu items
13
+ * @param {Object} [config.menuGroups] - Menu group definitions
14
+ * @param {Object} [config.api] - API endpoint definitions
15
+ * @param {Array} [config.widgets] - Dashboard widgets
16
+ * @param {Object} deps - Internal dependencies injected by admin-panel
17
+ * @param {Object} deps.registry - AdminRegistry instance
18
+ * @param {string} deps.adminPath - Admin panel base path
19
+ * @param {Object} deps.ctx - Plugin context
20
+ * @param {Function} deps.requireAuth - Auth middleware
21
+ * @param {Function} deps.optionalAuth - Optional auth middleware
22
+ * @param {Function} deps.serveAdminPanel - SPA HTML handler
23
+ */
24
+ function registerModule(config, deps) {
25
+ if (!config || typeof config !== 'object') {
26
+ throw new Error('registerModule requires a config object');
27
+ }
28
+
29
+ if (!config.id || typeof config.id !== 'string') {
30
+ throw new Error('registerModule requires a string "id" field');
31
+ }
32
+
33
+ const { registry, adminPath, ctx, requireAuth, optionalAuth, serveAdminPanel } = deps;
34
+
35
+ if (config.menuGroups) {
36
+ registerMenuGroups(config.menuGroups, registry);
37
+ }
38
+
39
+ if (config.pages) {
40
+ registerPages(config.id, config.pages, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
41
+ }
42
+
43
+ if (config.menu) {
44
+ registerMenuItems(config.menu, registry);
45
+ }
46
+
47
+ if (config.api) {
48
+ registerApiRoutes(config.id, config.api, { adminPath, ctx, requireAuth, optionalAuth });
49
+ }
50
+
51
+ if (config.widgets) {
52
+ registerWidgets(config.widgets, registry);
53
+ }
54
+ }
55
+
56
+ function registerPages(moduleId, pages, deps) {
57
+ if (!Array.isArray(pages)) {
58
+ throw new Error(`Module "${moduleId}": pages must be an array`);
59
+ }
60
+
61
+ const { registry, adminPath, ctx, optionalAuth, serveAdminPanel } = deps;
62
+
63
+ for (const page of pages) {
64
+ const pageId = page.id || moduleId;
65
+
66
+ if (!page.title || !page.path) {
67
+ throw new Error(`Module "${moduleId}", page "${pageId}": requires title and path`);
68
+ }
69
+
70
+ registry.registerPage(pageId, {
71
+ title: page.title,
72
+ path: page.path,
73
+ icon: page.icon,
74
+ description: page.description,
75
+ permission: page.permission,
76
+ });
77
+
78
+ if (page.component) {
79
+ registry.registerClientComponent(pageId, page.component);
80
+ }
81
+
82
+ ctx.addRoute('get', `${adminPath}${page.path}`, optionalAuth, serveAdminPanel);
83
+ }
84
+ }
85
+
86
+ function registerMenuItems(menu, registry) {
87
+ if (!Array.isArray(menu)) {
88
+ throw new Error('menu must be an array');
89
+ }
90
+
91
+ for (const item of menu) {
92
+ registry.registerMenuItem(item);
93
+ }
94
+ }
95
+
96
+ function registerMenuGroups(menuGroups, registry) {
97
+ if (typeof menuGroups !== 'object' || Array.isArray(menuGroups)) {
98
+ throw new Error('menuGroups must be an object');
99
+ }
100
+
101
+ for (const [id, config] of Object.entries(menuGroups)) {
102
+ registry.registerMenuGroup(id, config);
103
+ }
104
+ }
105
+
106
+ function registerApiRoutes(moduleId, apiConfig, deps) {
107
+ if (typeof apiConfig !== 'object') {
108
+ throw new Error(`Module "${moduleId}": api must be an object`);
109
+ }
110
+
111
+ const { adminPath, ctx, requireAuth, optionalAuth } = deps;
112
+ const prefix = apiConfig.prefix || '';
113
+ const defaultAuth = apiConfig.auth !== false;
114
+
115
+ if (!Array.isArray(apiConfig.routes)) {
116
+ throw new Error(`Module "${moduleId}": api.routes must be an array`);
117
+ }
118
+
119
+ for (const route of apiConfig.routes) {
120
+ if (!route.path || typeof route.handler !== 'function') {
121
+ throw new Error(`Module "${moduleId}": each API route requires path and handler`);
122
+ }
123
+
124
+ const method = (route.method || 'get').toLowerCase();
125
+ const fullPath = `${adminPath}/api${prefix}${route.path}`;
126
+ const useAuth = route.auth !== undefined ? route.auth : defaultAuth;
127
+ const authMiddleware = useAuth ? requireAuth : optionalAuth;
128
+
129
+ ctx.addRoute(method, fullPath, authMiddleware, route.handler);
130
+ }
131
+ }
132
+
133
+ function registerWidgets(widgets, registry) {
134
+ if (!Array.isArray(widgets)) {
135
+ throw new Error('widgets must be an array');
136
+ }
137
+
138
+ for (const widget of widgets) {
139
+ if (!widget.id) {
140
+ throw new Error('Each widget requires an id');
141
+ }
142
+ registry.registerWidget(widget.id, widget);
143
+ }
144
+ }
145
+
146
+ module.exports = { registerModule };
@@ -15,6 +15,7 @@ const { registerDashboardWidgets, generateDashboardComponent } = require('./modu
15
15
  const { registerDefaultBulkActions, generateBulkActionsComponent } = require('./modules/bulk-actions');
16
16
  const { registerDefaultPages, createCustomPageApiHandlers, generateCustomPageComponent } = require('./modules/custom-pages');
17
17
  const { registerModelMenuItems, registerSystemMenuItems, generateMenuComponent } = require('./modules/menu');
18
+ const { registerModule } = require('./core/admin-module');
18
19
 
19
20
  /**
20
21
  * Admin Panel Plugin Factory
@@ -64,10 +65,15 @@ function adminPanelPlugin(options = {}) {
64
65
  version: '2.0.0',
65
66
  description: 'Modular admin panel for Webspresso with extensions support',
66
67
 
67
- // CSP requirements for Quill.js rich text editor
68
+ // CSP requirements for admin panel scripts
68
69
  csp: {
69
70
  styleSrc: ['https://cdn.quilljs.com'],
70
- scriptSrc: ['https://cdn.quilljs.com'],
71
+ scriptSrc: [
72
+ 'https://cdn.quilljs.com',
73
+ 'https://unpkg.com',
74
+ 'https://cdn.tailwindcss.com',
75
+ ],
76
+ connectSrc: ['https://unpkg.com', 'https://cdn.tailwindcss.com'],
71
77
  },
72
78
  enabled,
73
79
  registry, // Expose registry for external configuration
@@ -78,6 +84,20 @@ function adminPanelPlugin(options = {}) {
78
84
  serveAdminPanel,
79
85
  requireAuth,
80
86
  optionalAuth,
87
+ registerModule(config) {
88
+ if (!this._ctx) {
89
+ throw new Error('registerModule can only be called during or after onRoutesReady');
90
+ }
91
+ return registerModule(config, {
92
+ registry,
93
+ adminPath,
94
+ ctx: this._ctx,
95
+ requireAuth,
96
+ optionalAuth,
97
+ serveAdminPanel,
98
+ });
99
+ },
100
+ _ctx: null,
81
101
  },
82
102
 
83
103
  /**
@@ -101,6 +121,7 @@ function adminPanelPlugin(options = {}) {
101
121
  return;
102
122
  }
103
123
 
124
+ this.api._ctx = ctx;
104
125
  const { app } = ctx;
105
126
 
106
127
  // Create and register AdminUser model
@@ -37,6 +37,7 @@ function siteAnalyticsPlugin(options = {}) {
37
37
 
38
38
  csp: {
39
39
  scriptSrc: ['https://cdn.jsdelivr.net'],
40
+ connectSrc: ['https://cdn.jsdelivr.net'],
40
41
  },
41
42
 
42
43
  register(ctx) {
@@ -59,42 +60,40 @@ function siteAnalyticsPlugin(options = {}) {
59
60
  return;
60
61
  }
61
62
 
62
- const registry = adminApi.getRegistry();
63
- const adminPath = adminApi.getAdminPath();
64
- const { requireAuth } = adminApi;
65
63
  const knex = db.knex || db;
66
-
67
- // Register admin page
68
- registry.registerPage('analytics', {
69
- title: 'Analytics',
70
- path: '/analytics',
71
- icon: 'chart',
72
- });
73
-
74
- // Register menu item
75
- registry.registerMenuItem({
76
- id: 'analytics',
77
- label: 'Analytics',
78
- path: '/analytics',
79
- icon: 'chart',
80
- order: 2,
81
- });
82
-
83
- // Register client-side component
84
- registry.registerClientComponent('analytics', generateAnalyticsComponent());
85
-
86
- // API routes
87
64
  const handlers = createAnalyticsApiHandlers({ knex, tableName });
88
65
 
89
- ctx.addRoute('get', `${adminPath}/api/analytics/stats`, requireAuth, handlers.getStats);
90
- ctx.addRoute('get', `${adminPath}/api/analytics/views-over-time`, requireAuth, handlers.getViewsOverTime);
91
- ctx.addRoute('get', `${adminPath}/api/analytics/top-pages`, requireAuth, handlers.getTopPages);
92
- ctx.addRoute('get', `${adminPath}/api/analytics/bot-activity`, requireAuth, handlers.getBotActivity);
93
- ctx.addRoute('get', `${adminPath}/api/analytics/countries`, requireAuth, handlers.getCountries);
94
- ctx.addRoute('get', `${adminPath}/api/analytics/recent`, requireAuth, handlers.getRecent);
66
+ adminApi.registerModule({
67
+ id: 'analytics',
95
68
 
96
- // Serve admin SPA for the analytics route
97
- ctx.addRoute('get', `${adminPath}/analytics`, adminApi.optionalAuth, adminApi.serveAdminPanel);
69
+ pages: [{
70
+ id: 'analytics',
71
+ title: 'Analytics',
72
+ path: '/analytics',
73
+ icon: 'chart',
74
+ component: generateAnalyticsComponent(),
75
+ }],
76
+
77
+ menu: [{
78
+ id: 'analytics',
79
+ label: 'Analytics',
80
+ path: '/analytics',
81
+ icon: 'chart',
82
+ order: 2,
83
+ }],
84
+
85
+ api: {
86
+ prefix: '/analytics',
87
+ routes: [
88
+ { method: 'get', path: '/stats', handler: handlers.getStats },
89
+ { method: 'get', path: '/views-over-time', handler: handlers.getViewsOverTime },
90
+ { method: 'get', path: '/top-pages', handler: handlers.getTopPages },
91
+ { method: 'get', path: '/bot-activity', handler: handlers.getBotActivity },
92
+ { method: 'get', path: '/countries', handler: handlers.getCountries },
93
+ { method: 'get', path: '/recent', handler: handlers.getRecent },
94
+ ],
95
+ },
96
+ });
98
97
  },
99
98
  };
100
99
  }
@@ -312,10 +312,11 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
312
312
  * @param {Object} options.middlewares - Named middleware registry
313
313
  * @param {Object} options.pluginManager - Plugin manager instance
314
314
  * @param {boolean} options.silent - Suppress console output
315
+ * @param {Object} options.db - Database instance (exposed as ctx.db in load/meta)
315
316
  * @returns {Array} Route metadata for plugins
316
317
  */
317
318
  function mountPages(app, options) {
318
- const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false } = options;
319
+ const { pagesDir, nunjucks, middlewares = {}, pluginManager = null, silent = false, db = null } = options;
319
320
  const isDev = process.env.NODE_ENV !== 'production';
320
321
  const log = silent ? () => {} : console.log.bind(console);
321
322
 
@@ -453,6 +454,7 @@ function mountPages(app, options) {
453
454
  const ctx = {
454
455
  req,
455
456
  res,
457
+ db,
456
458
  path: route.routePath,
457
459
  file: route.file,
458
460
  routeDir: route.routeDir,
@@ -351,6 +351,7 @@ class PluginManager {
351
351
  app: context.app,
352
352
  options: plugin._options || {},
353
353
  nunjucksEnv: context.nunjucksEnv,
354
+ db: context.options?.db ?? null,
354
355
 
355
356
  /**
356
357
  * Get another plugin's API
package/src/server.js CHANGED
@@ -164,6 +164,7 @@ function haltOnTimedout(req, res, next) {
164
164
  * @param {Function|string} options.errorPages.timeout - Custom timeout handler or template path
165
165
  * @param {string|boolean} options.timeout - Request timeout (default: '30s', false to disable)
166
166
  * @param {Object} options.auth - Authentication manager instance (from createAuth)
167
+ * @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
167
168
  * @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
168
169
  */
169
170
  function createApp(options = {}) {
@@ -340,7 +341,8 @@ function createApp(options = {}) {
340
341
  nunjucks: nunjucksEnv,
341
342
  middlewares,
342
343
  pluginManager,
343
- silent: isTest
344
+ silent: isTest,
345
+ db: options.db ?? null
344
346
  });
345
347
 
346
348
  // Set route metadata in plugin manager
@@ -354,6 +356,7 @@ function createApp(options = {}) {
354
356
  app,
355
357
  nunjucksEnv,
356
358
  options,
359
+ db: options.db ?? null,
357
360
  routes: pluginManager.routes,
358
361
  usePlugin: (n) => pluginManager.getPluginAPI(n),
359
362
  addHelper: (n, fn) => pluginManager.registeredHelpers.set(n, fn),