webspresso 0.0.100 → 0.0.102

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 (88) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +3 -1
  3. package/.agents/ARCHITECTURE.md +11 -0
  4. package/.agents/AUTH.md +63 -0
  5. package/.agents/CLI.md +81 -0
  6. package/.agents/CONVENTIONS.md +4 -0
  7. package/.agents/EMAIL.md +84 -2
  8. package/.agents/EVENTS.md +179 -0
  9. package/.agents/ORM.md +59 -0
  10. package/.agents/PLUGINS.md +32 -6
  11. package/.agents/ROUTING.md +163 -3
  12. package/.agents/skills/webspresso-framework/SKILL.md +32 -0
  13. package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
  14. package/AGENTS.md +7 -0
  15. package/README.md +18 -5
  16. package/bin/commands/add-tailwind.js +10 -0
  17. package/bin/commands/build.js +34 -0
  18. package/bin/commands/db-makemigrations.js +83 -0
  19. package/bin/commands/list.js +96 -0
  20. package/bin/commands/make-command.js +93 -0
  21. package/bin/commands/make-event.js +80 -0
  22. package/bin/commands/make-factory.js +72 -0
  23. package/bin/commands/make-mail.js +93 -0
  24. package/bin/commands/make-policy.js +106 -0
  25. package/bin/commands/routes.js +8 -0
  26. package/bin/commands/run.js +195 -0
  27. package/bin/webspresso.js +17 -0
  28. package/core/applySchema.js +18 -1
  29. package/core/auth/bouncer.js +196 -0
  30. package/core/auth/index.js +10 -0
  31. package/core/auth/middleware.js +15 -1
  32. package/core/commands/define-command.js +48 -0
  33. package/core/commands/index.js +18 -0
  34. package/core/commands/registry.js +156 -0
  35. package/core/commands/runner.js +76 -0
  36. package/core/compression/compressible.js +1 -1
  37. package/core/compression/middleware.js +35 -251
  38. package/core/content/service.js +8 -2
  39. package/core/events/discovery.js +142 -0
  40. package/core/events/dispatcher.js +552 -0
  41. package/core/events/index.js +23 -0
  42. package/core/events/signal.js +92 -0
  43. package/core/forms/flash.js +234 -0
  44. package/core/forms/form-builder.js +214 -0
  45. package/core/forms/index.js +16 -0
  46. package/core/mail/index.js +23 -0
  47. package/core/mail/mailable.js +250 -0
  48. package/core/mail/mailer.js +184 -0
  49. package/core/mail/transports.js +238 -0
  50. package/core/orm/factory.js +250 -0
  51. package/core/orm/index.js +6 -0
  52. package/core/orm/migrations/diff.js +185 -0
  53. package/core/orm/migrations/index.js +22 -4
  54. package/core/orm/model.js +4 -0
  55. package/index.d.ts +422 -1
  56. package/index.js +111 -16
  57. package/package.json +9 -5
  58. package/plugins/admin-panel/api.js +509 -3
  59. package/plugins/admin-panel/index.js +29 -0
  60. package/plugins/content/models/content-entry.js +4 -3
  61. package/plugins/content/models/content-type.js +4 -3
  62. package/plugins/cors.js +2 -2
  63. package/plugins/csrf/index.js +1 -1
  64. package/plugins/email/index.js +48 -0
  65. package/plugins/email/preview-handler.js +555 -0
  66. package/plugins/email/service.js +23 -12
  67. package/plugins/email/transport.js +30 -11
  68. package/plugins/file-manager/index.js +13 -0
  69. package/plugins/index.js +2 -0
  70. package/plugins/lazy.js +258 -0
  71. package/plugins/redirect/index.js +1 -1
  72. package/src/api/api-loader.js +18 -2
  73. package/src/api/validation-i18n.js +231 -0
  74. package/src/app-context.js +48 -6
  75. package/src/dev-reload/index.js +9 -1
  76. package/src/discovery/route-manifest.js +197 -0
  77. package/src/discovery/scan-routes.js +80 -5
  78. package/src/file-router.js +153 -91
  79. package/src/helpers.js +143 -0
  80. package/src/njk-frontmatter.js +17 -1
  81. package/src/pages/page-loader.js +1 -1
  82. package/src/plugin-manager.js +12 -0
  83. package/src/routing/cascading.js +14 -10
  84. package/src/routing/route-table.js +1 -1
  85. package/src/server.js +243 -34
  86. package/src/services/builtins/system.js +4 -6
  87. package/src/services/registry.js +12 -2
  88. package/src/template/precompile.js +247 -0
package/.agents/ADMIN.md CHANGED
@@ -46,6 +46,31 @@ export default defineModel({
46
46
  sortableColumns: ['name', 'price'], // Optional whitelist of sortable columns
47
47
  sortable: true, // Set false to disable table sorting completely
48
48
  readOnly: false, // Disable create/edit/delete actions
49
+
50
+ // Django Admin-inspired Features:
51
+ inlines: [ // Parent-child relational editing (Tabular/Stacked)
52
+ {
53
+ model: 'ProductVariant',
54
+ foreignKey: 'product_id',
55
+ fields: ['sku', 'price', 'inventory'],
56
+ title: 'Variants',
57
+ },
58
+ ],
59
+ listEditable: ['price', 'status'], // Direct table row quick-edit without opening edit form
60
+ dateHierarchy: 'created_at', // Period drilldown navigation (Years -> Months -> Days)
61
+ listFilter: ['status'], // Faceted filtering with record counts per value
62
+ actions: [ // Declarative model-level bulk operations
63
+ {
64
+ id: 'mark-active',
65
+ label: 'Mark as Active',
66
+ handler: async ({ ids, repo, db, req, user }) => {
67
+ for (const id of ids) {
68
+ await repo.update(id, { status: 'active' });
69
+ }
70
+ return { updated: ids.length };
71
+ },
72
+ },
73
+ ],
49
74
  },
50
75
  });
51
76
  ```
@@ -68,13 +93,31 @@ Column sortability can be configured per-column:
68
93
  The Admin SPA communicates with the backend via JSON REST endpoints:
69
94
 
70
95
  - `GET /_admin/api/models` — List all models exposed to admin with schemas & metadata.
71
- - `GET /_admin/api/models/:modelName/records` — List paginated records (query params: `page`, `limit`, `search`, `filter`, `sort`).
72
- - `GET /_admin/api/models/:modelName/records/:id` — Retrieve single record details.
73
- - `POST /_admin/api/models/:modelName/records` — Create new record.
74
- - `PUT /_admin/api/models/:modelName/records/:id` — Update existing record.
96
+ - `GET /_admin/api/models/:modelName/records` — List paginated records (query params: `page`, `limit`, `search`, `filter`, `sort`, `year`, `month`, `day`).
97
+ - `GET /_admin/api/models/:modelName/records/:id` — Retrieve single record details (auto-loads `_inlines` if defined).
98
+ - `POST /_admin/api/models/:modelName/records` — Create new record (syncs `_inlines` automatically).
99
+ - `PUT /_admin/api/models/:modelName/records/:id` — Update existing record (syncs `_inlines` updates, additions, and deletions with `_delete: true`).
75
100
  - `DELETE /_admin/api/models/:modelName/records/:id` — Delete record (soft or hard delete).
76
101
  - `POST /_admin/api/models/:modelName/records/:id/restore` — Restore soft-deleted record.
77
102
 
103
+ ### Inline Model Relational Endpoints (Django Inlines)
104
+ - `GET /_admin/api/models/:model/records/:id/inlines/:inlineModel` — List child items belonging to parent record.
105
+ - `POST /_admin/api/models/:model/records/:id/inlines/:inlineModel` — Create a child item attached to parent.
106
+ - `PUT /_admin/api/models/:model/records/:id/inlines/:inlineModel/:inlineId` — Update specific child item.
107
+ - `DELETE /_admin/api/models/:model/records/:id/inlines/:inlineModel/:inlineId` — Delete child item.
108
+
109
+ ### Fast Inline & Batch Editing (Django `list_editable`)
110
+ - `PATCH /_admin/api/models/:model/records/:id/quick-edit` — Quickly update allowed `listEditable` columns for a single row without full form submission.
111
+ - `PATCH /_admin/api/models/:model/records/batch-edit` — Save multiple table row edits simultaneously (`{ updates: [{ id, changes }] }`).
112
+
113
+ ### Date Hierarchy & Faceted Filters (Django `date_hierarchy` & `list_filter`)
114
+ - `GET /_admin/api/models/:model/date-hierarchy` — Drilldown periods with counts (`?year=...&month=...`).
115
+ - `GET /_admin/api/models/:model/filter-facets` — Retrieve distinct values with record counts for `listFilter` columns.
116
+
117
+ ### Model Actions (Django Actions)
118
+ - `POST /_admin/api/models/:model/actions/:actionId` — Execute declarative action on selected record IDs (`{ ids: [...] }`).
119
+
120
+
78
121
  ---
79
122
 
80
123
  ## 4. Custom Pages (`registerModule` / `registerPageDir`)
package/.agents/AGENTS.md CHANGED
@@ -17,6 +17,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
17
17
  | **Admin Panel (Page, Widget, Action, UI)** | [`.agents/ADMIN.md`](ADMIN.md) | `plugins/admin-panel/` |
18
18
  | **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](AUTH.md) | `core/auth/` |
19
19
  | **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](QUEUE.md) | `jobs/` |
20
+ | **Decoupled Events & Signals** | [`.agents/EVENTS.md`](EVENTS.md) | `core/events/`, `listeners/` |
20
21
  | **API Signatures & Types Verification** | [`index.d.ts`](../index.d.ts) | **Canonical Ground Truth** |
21
22
 
22
23
  ---
@@ -37,7 +38,7 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
37
38
  This `.agents/` customization root is organized into modular topic guides:
38
39
 
39
40
  1. **[File-Based Routing & API Routes](.agents/ROUTING.md)** — File-based page routes (`pages/`), method-suffixed API routes (`pages/api/*.get.js`, `*.post.js`), parameter brackets (`[id]`), Zod request schemas, and the strict prohibition of manual `routes/` directories or `app.get()`.
40
- 2. **[Architecture & Structure](.agents/ARCHITECTURE.md)** — File-based routing (`pages/`), Asset Management & Versioning (`AssetManager`, `fsy.asset`), ORM models & `zdb` schemas (`core/orm`), Mithril.js Admin Panel SPA (`plugins/admin-panel`), and Plugin Ecosystem.
41
+ 2. **[Architecture & Structure](.agents/ARCHITECTURE.md)** — File-based routing (`pages/`), Cold Start & AOT Optimizations (`discoverAppRoutes`, `lazyRoutes: true`, `routes.manifest.json`, `templates.compiled.js`, `lazyPlugin`), Asset Management & Versioning (`AssetManager`, `fsy.asset`), ORM models & `zdb` schemas (`core/orm`), Mithril.js Admin Panel SPA (`plugins/admin-panel`), and Plugin Ecosystem.
41
42
  3. **[Services Layer](.agents/SERVICES.md)** — File-based auto-discovery (`services/`), Zod validation, declarative `auth` & RBAC guards, automatic ACID transaction propagation (`transaction: true`), timeouts, and memoization.
42
43
  4. **[Exceptions & Error Handling](.agents/ERRORS.md)** — Django-inspired framework exception hierarchy (`WebspressoError`, `HttpError`, `ValidationError`, `SecurityError`, `RequestAbortedError`), central error boundary, `app.setErrorHandler()`, and production error masking.
43
44
  5. **[Graceful Shutdown & Lifecycle](.agents/SHUTDOWN.md)** — `ShutdownManager`, `NodeHttpAdapter`, connection draining, force close mode, and reverse-order plugin disposer cleanup.
@@ -54,6 +55,7 @@ This `.agents/` customization root is organized into modular topic guides:
54
55
  16. **[Background Job Queue](.agents/QUEUE.md)** — In-Memory, Knex Database, and Distributed Redis queue adapters, job auto-discovery (`jobs/`), retries, and graceful draining.
55
56
  17. **[SSR Streaming & Chunked Transfer](.agents/STREAMING.md)** — Native HTTP chunked streaming (`res.renderStream()`), deferred data loader slots (`defer: { key: promise }`), and TTFB optimization.
56
57
  18. **Polar billing production checklist** — [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md) (env, webhooks, rate limits, external tier hooks, CSP). Plugin reference: [PLUGINS.md § polarPlugin](PLUGINS.md).
58
+ 19. **[Decoupled Events & Signals Architecture](.agents/EVENTS.md)** — Zero-dependency enterprise event dispatcher (`core/events`), sync/async/parallel/serial/waterfall dispatch, wildcard listeners (`order.*`), Django-style `Signal` and sender filtering, queue bridging, and listener auto-discovery.
57
59
 
58
60
  ---
59
61
 
@@ -23,6 +23,12 @@
23
23
  - Discovered routes are compiled and sorted in deterministic priority order: Static $\to$ Deep Dynamic $\to$ Shallow Dynamic $\to$ Catch-All (`*`).
24
24
  - Detects `METHOD + PATH` collision conflicts at startup and throws descriptive errors.
25
25
  - Inspectable via `app.routes.list()` for CLI tools, documentation generation, and debugging.
26
+ - **High-Performance Architecture & Cold-Start Optimization**:
27
+ - **Single-Pass Unified Route Discovery (`discoverAppRoutes`)**: Discovers Page routes, API routes, and Route Groups in a single filesystem traversal pass, avoiding redundant tree walks during startup.
28
+ - **Dev-Mode On-Demand (JIT) Route Evaluation (`lazyRoutes: true`)**: In development (`NODE_ENV !== 'production'`), route handler modules (`pages/**/*.js`) are loaded on first incoming HTTP request instead of eager-evaluating all routes at boot, reducing dev boot time to ~15ms on large projects.
29
+ - **AOT Route Manifest (`webspresso build` & `routesManifest`)**: Production builds generate `.webspresso/routes.manifest.json`. When present (or configured via `createApp({ routesManifest: true })`), zero directory scanning or route sorting occurs at runtime.
30
+ - **AOT Nunjucks Template Precompilation (`precompileTemplates` & `precompiledTemplates`)**: `webspresso build` compiles all `.njk` templates in `pages/` and `views/` into `.webspresso/templates.compiled.js`. In production, templates execute without runtime parser/AST generation or disk reads.
31
+ - **On-Demand Lazy Plugins (`lazyPlugin`, `defineLazyPlugin`)**: Heavy plugins (e.g. `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`) mount Express proxy routers and hydrate in memory only when a matching HTTP request or service call arrives.
26
32
  - **Templating & Helpers**: Nunjucks (`.njk`) templates rendered with layouts (e.g. `views/layout.njk`). Access helpers via `fsy` object in templates.
27
33
  - **Asset Management & Cache-Busting (`assets`, `AssetManager`, `fsy`)**:
28
34
  - Configured via `createApp({ assets: { version, manifestPath, prefix, publicDir } })` or `configureAssets()`.
@@ -115,6 +121,11 @@ Plugins expose `name`, `version`, `dependencies`, and lifecycle hooks (`register
115
121
  | `restResourcePlugin` | ORM / REST API | Auto RESTful CRUD endpoints from models with eager loading & sanitization |
116
122
  | `xlsxPlugin` | Excel / Export | Excel spreadsheet generation, parsing, ORM streaming export (`res.xlsx`), and services |
117
123
  | `csvPlugin` | CSV / Export | RFC 4180 CSV generator, parser, Excel UTF-8 BOM, streaming writer (`res.csv`), and services |
124
+ | `lazyPlugin` | Core / Performance | On-demand plugin hydration wrapper deferring plugin setup until first request/call |
125
+
126
+ - **On-Demand Lazy Plugins (`lazyPlugin`, `defineLazyPlugin`)**:
127
+ - Wrap any plugin via `lazyPlugin(() => heavyPlugin(), { prefix: '/path', services: ['service.name'] })`.
128
+ - Built-in shorthand support on resource-heavy plugins: `adminPanelPlugin({ db, lazy: true })` and `fileManagerPlugin({ lazy: true })`.
118
129
 
119
130
  ---
120
131
 
package/.agents/AUTH.md CHANGED
@@ -514,5 +514,68 @@ auth.on('sessions:revoked_all', ({ userId, count }) => {
514
514
  });
515
515
  ```
516
516
 
517
+ ---
518
+
519
+ ## 9. Authorization & Policies (AdonisJS Bouncer & Django Permissions-inspired)
520
+
521
+ Webspresso provides an authorization layer combining AdonisJS Bouncer policies/gates with Django-style permission matrices and superuser bypass:
522
+
523
+ ### 9.1 Defining Policies & Gates
524
+ Define domain policies and simple gates using `definePolicy` and `defineGate`:
525
+
526
+ ```javascript
527
+ const { definePolicy, defineGate } = require('webspresso');
528
+
529
+ // 1. Resource Policy
530
+ definePolicy('Post', {
531
+ view: (user, post) => true,
532
+ update: (user, post) => user && (user.id === post.user_id || user.role === 'admin'),
533
+ delete: (user, post) => user && user.role === 'admin',
534
+ });
535
+
536
+ // 2. Named Gate
537
+ defineGate('admin-only', (user) => Boolean(user && user.role === 'admin'));
538
+ ```
539
+
540
+ ### 9.2 Request Bouncer (`req.bouncer`)
541
+ Incoming requests automatically receive `req.bouncer` bound to `req.user`:
542
+
543
+ ```javascript
544
+ // Inside API Route or Loader:
545
+ if (req.bouncer.allows('Post.update', post)) {
546
+ // Authorized
547
+ }
548
+
549
+ if (req.bouncer.denies('admin-only')) {
550
+ // Denied
551
+ }
552
+
553
+ // Throws ForbiddenError (403) or UnauthorizedError (401) on failure:
554
+ req.bouncer.authorize('Post.update', post);
555
+
556
+ // Inline rule check:
557
+ req.bouncer.authorize((user) => user.isVip);
558
+ ```
559
+
560
+ ### 9.3 Django Permissions Matrix & Superuser Bypass
561
+ Users with permissions arrays or superuser flags are evaluated automatically:
562
+ - `user.permissions = ['post.create', 'post.update']`
563
+ - Wildcards: `user.permissions = ['post.*']` or `['*']`
564
+ - Django Superuser: `user.isSuperuser = true` or `user.is_superuser = true` automatically bypasses all permission checks.
565
+ - Custom methods: `user.hasPerm(perm, resource)` or `user.hasPermission(...)`.
566
+
567
+ ### 9.4 Nunjucks Template Helpers (`fsy.can`, `fsy.cannot`)
568
+ Check permissions directly in `.njk` templates:
569
+
570
+ ```jinja
571
+ {% if fsy.can('Post.update', post) %}
572
+ <a href="/posts/{{ post.id }}/edit" class="btn">Edit Post</a>
573
+ {% endif %}
574
+
575
+ {% if fsy.cannot('admin-only') %}
576
+ <p class="text-muted">Admin access required</p>
577
+ {% endif %}
578
+ ```
579
+
517
580
 
518
581
 
package/.agents/CLI.md CHANGED
@@ -354,3 +354,84 @@ npx webspresso deploy:check --json
354
354
  3. **Container Hardening**: Ensures `Dockerfile` runs as a non-root `USER`, configures a `HEALTHCHECK`, and uses multi-stage caching.
355
355
  4. **Database & Migrations**: Audits for pending unapplied migrations and warns against ephemeral SQLite storage.
356
356
  5. **Server Optimizations**: Validates HTTP compression (`compression: true`), graceful shutdown, and health check endpoints.
357
+
358
+ ---
359
+
360
+ ## 18. Custom Management Commands (`run`, `command:run`, `defineCommand`) [Django manage.py & Adonis Ace]
361
+
362
+ Create and execute isolated management scripts, background cron tasks, data migrations, or maintenance routines with full framework dependency injection (`db`, `queue`, `services`, `mailer`, `events`, `logger`, `args`, `flags`):
363
+
364
+ ```bash
365
+ # List all custom management commands in commands/
366
+ npx webspresso run
367
+
368
+ # Execute a custom command with positional arguments and options
369
+ npx webspresso run sync-stripe --dry-run --limit 100
370
+ ```
371
+
372
+ ### Defining a Custom Command (`commands/<name>.js`):
373
+
374
+ ```javascript
375
+ const { defineCommand } = require('webspresso');
376
+
377
+ module.exports = defineCommand({
378
+ name: 'sync-stripe',
379
+ description: 'Synchronize customer subscriptions from Stripe',
380
+ options: [
381
+ { flags: '-d, --dry-run', description: 'Simulate without committing changes', defaultValue: false },
382
+ { flags: '-l, --limit <count>', description: 'Maximum customers to sync', defaultValue: 100 },
383
+ ],
384
+ handle: async ({ db, queue, services, mailer, events, logger, args, flags }) => {
385
+ logger.log('Starting sync, dryRun:', flags.dryRun);
386
+
387
+ const userRepo = db.getRepository('User');
388
+ const users = await userRepo.find({ active: true });
389
+
390
+ // Events, mailer, and background jobs are fully accessible:
391
+ events.emit('stripe:sync-started', { count: users.length });
392
+
393
+ return { synced: users.length };
394
+ },
395
+ });
396
+ ```
397
+
398
+ ---
399
+
400
+ ## 19. Command Palette Index (`list`, `commands`)
401
+
402
+ Print an organized catalog of all built-in commands (scaffolding, database, server, developer tools) alongside all auto-discovered custom commands in your project:
403
+
404
+ ```bash
405
+ npx webspresso list
406
+ # or
407
+ npx webspresso commands
408
+ ```
409
+
410
+ ---
411
+
412
+ ## 20. Advanced Scaffolding Generators (`make:*`) [Adonis Ace style]
413
+
414
+ Rapidly generate boilerplate code following Webspresso architectural conventions:
415
+
416
+ ```bash
417
+ # 1. Custom Management Command
418
+ npx webspresso make:command users:cleanup --desc "Purge unverified user accounts"
419
+ # Outputs: commands/users/cleanup.js
420
+
421
+ # 2. Authorization Policy (Bouncer)
422
+ npx webspresso make:policy Article
423
+ # Outputs: policies/article.policy.js
424
+
425
+ # 3. Model Factory (Testing & Seeding)
426
+ npx webspresso make:factory Product
427
+ # Outputs: factories/product.factory.js
428
+
429
+ # 4. Decoupled Event Listener
430
+ npx webspresso make:event order:completed
431
+ # Outputs: listeners/order-completed.js
432
+
433
+ # 5. Declarative Mailable Class
434
+ npx webspresso make:mail Welcome
435
+ # Outputs: emails/WelcomeEmail.js
436
+ ```
437
+
@@ -44,3 +44,7 @@
44
44
  - **ALWAYS** use published `package.json` export paths: `require('webspresso/core/auth')`, `require('webspresso/core/orm')`, `require('webspresso/plugins/polar/src/webhooks')`.
45
45
  - **NEVER** rely on deep relative paths from `node_modules/webspresso/...` or append `/index.js` unless documenting a pre-0.0.95 workaround.
46
46
  - Smoke test: `tests/unit/package-exports.test.js`.
47
+ 14. **Tailwind CSS v4 Standard (NO v3 PostCSS Boilerplate)**:
48
+ - **ALWAYS** prefer Tailwind CSS v4 using `npx webspresso add tailwind --v4` (powered by Oxide and `@tailwindcss/cli`).
49
+ - **ALWAYS** use CSS-first `@import "tailwindcss";` in `src/input.css` and compile to `public/css/style.css`.
50
+ - **NEVER** create redundant `tailwind.config.js` or `postcss.config.js` files unless custom legacy PostCSS plugins are specifically required.
package/.agents/EMAIL.md CHANGED
@@ -102,11 +102,92 @@ emailApi.registerTemplate('notification', '<mjml><mj-body>...</mj-body></mjml>')
102
102
  const isReady = await emailApi.verifyConnection();
103
103
  const logs = await emailApi.queryLogs({ status: 'sent', limit: 20 });
104
104
  await emailApi.purgeLogs({ olderThan: '30d' });
105
+
106
+ // 6. In-memory outbox inspection & clearing (memory backend / tests)
107
+ const outbox = emailApi.getOutbox();
108
+ const lastSent = emailApi.lastMessage();
109
+ emailApi.clearOutbox();
110
+ ```
111
+
112
+ ---
113
+
114
+ ## 5. Declarative `Mailable` Classes (AdonisJS-style)
115
+
116
+ Define structured, reusable, object-oriented emails by extending `Mailable`:
117
+
118
+ ```javascript
119
+ import { Mailable } from 'webspresso';
120
+
121
+ export class WelcomeEmail extends Mailable {
122
+ constructor(user) {
123
+ super();
124
+ this.user = user;
125
+ }
126
+
127
+ build() {
128
+ this.to(this.user.email, this.user.name)
129
+ .from('team@myapp.com', 'Acme Team')
130
+ .subject(`Welcome to Acme, ${this.user.name}!`)
131
+ .view('welcome', { user: this.user }) // references registered template
132
+ .attach({
133
+ filename: 'welcome-guide.pdf',
134
+ content: Buffer.from('...'),
135
+ });
136
+ }
137
+ }
138
+
139
+ // In route handlers or services:
140
+ await req.mailer.send(new WelcomeEmail(user));
141
+ // Or via email API:
142
+ await emailApi.send(new WelcomeEmail(user));
143
+ ```
144
+
145
+ ### Fluent Mailable Methods
146
+
147
+ - `to(address, name?)`, `cc(address, name?)`, `bcc(address, name?)`, `replyTo(address, name?)`, `from(address, name?)`
148
+ - `subject(title)`
149
+ - `view(templateName, data?)` or `template(templateName)`
150
+ - `html(htmlContent)`, `text(textContent)`
151
+ - `attach({ filename, content, path, contentType })`
152
+ - `header(name, value)`
153
+ - `prepare()` and `build()` lifecycle hooks
154
+ - `toMessage()` serializes the definition into a message payload
155
+
156
+ ---
157
+
158
+ ## 6. Pluggable Transports (Django-style Backends)
159
+
160
+ Configure backends via `backend`: `'smtp' | 'memory' | 'console' | 'file'`:
161
+
162
+ ```javascript
163
+ const email = emailPlugin({
164
+ backend: process.env.NODE_ENV === 'production' ? 'smtp' : 'memory',
165
+ fileDir: './storage/emails', // when backend: 'file'
166
+ devInbox: true, // enables developer inbox web UI
167
+ });
105
168
  ```
106
169
 
170
+ - **`MemoryTransport`**: Stores messages in an in-memory inspectable outbox (`mailer.getOutbox()`). Automatically chosen in test environments.
171
+ - **`ConsoleTransport`**: Pretty-prints sent emails to stdout/logger for zero-setup terminal debugging.
172
+ - **`FileTransport`**: Writes JSON/EML email files to a local directory for disk-based inspection.
173
+ - **`NodemailerTransport`**: Delivers real emails via SMTP or custom transport in production.
174
+
107
175
  ---
108
176
 
109
- ## 5. Auth Email Bridge (`authEmails`)
177
+ ## 7. Developer Mail Preview & Dev Inbox (`/_webspresso/mail-preview`)
178
+
179
+ When `devInbox: true` or running in `development` mode, Webspresso automatically mounts a local inbox viewer:
180
+
181
+ - **Web UI Route**: `GET /_webspresso/mail-preview` (responsive inbox viewer with desktop, tablet, and mobile viewport switcher)
182
+ - **JSON List Route**: `GET /_webspresso/mail-preview/api/messages`
183
+ - **Single Message Route**: `GET /_webspresso/mail-preview/api/messages/:id`
184
+ - **Iframe HTML View**: `GET /_webspresso/mail-preview/api/messages/:id/html`
185
+ - **Clear Inbox Route**: `POST /_webspresso/mail-preview/api/clear`
186
+ - **Template Preview**: `POST /_webspresso/mail-preview/api/preview`
187
+
188
+ ---
189
+
190
+ ## 8. Auth Email Bridge (`authEmails`)
110
191
 
111
192
  Automatically wires password reset and email verification flows with `AuthManager`:
112
193
 
@@ -131,7 +212,7 @@ const email = emailPlugin({
131
212
 
132
213
  ---
133
214
 
134
- ## 6. Admin Panel UI & Database Logs
215
+ ## 9. Admin Panel UI & Database Logs
135
216
 
136
217
  When `includeAdminPage: true` and `adminPanelPlugin` are present, an interactive Email Management module is mounted at `/_admin/email`:
137
218
 
@@ -139,3 +220,4 @@ When `includeAdminPage: true` and `adminPanelPlugin` are present, an interactive
139
220
  - **Test Sender**: Send test emails directly from the Admin Panel to verify SMTP configuration.
140
221
  - **Audit Logs Inspector**: Filter sent email logs by status (`sent`, `failed`), view error tracebacks, and purge historic logs.
141
222
  - **Database Schema**: Log entries are stored in `email_logs` table (`id`, `to_email`, `subject`, `template_name`, `status`, `error`, `created_at`).
223
+
@@ -0,0 +1,179 @@
1
+ # Webspresso Events & Signals Architecture
2
+
3
+ [← Back to AGENTS.md](.agents/AGENTS.md)
4
+
5
+ Webspresso provides an enterprise, zero-dependency **Decoupled Event & Signals System** (`core/events`), combining **AdonisJS Events** (wildcard matching, async parallel/serial/waterfall dispatch, queue bridging, and listener auto-discovery) with **Django Signals** (declarative `Signal` channels, sender filtering, and ORM model signals).
6
+
7
+ ---
8
+
9
+ ## 1. Overview & Key Capabilities
10
+
11
+ - **Zero External Dependencies**: Built entirely with native Node.js standard modules (`events`, `path`, `fs`).
12
+ - **Synchronous & Asynchronous Emission**:
13
+ - `events.emit(name, data)`: Fast sync/fire-and-forget dispatch.
14
+ - `await events.emitAsync(name, data)`: Parallel concurrent execution (`Promise.allSettled`).
15
+ - `await events.emitSerial(name, data)`: Sequential execution in priority order.
16
+ - `await events.emitWaterfall(name, initialData)`: Sequential pipeline where output of listener $N$ feeds listener $N+1$.
17
+ - **Wildcard & Pattern Matching**:
18
+ - `events.on('order.*', (data, meta) => ...)`
19
+ - `events.on('system.**', (data, meta) => ...)`
20
+ - `events.onAny((eventName, data, meta) => ...)`
21
+ - **Django-Style Signals & Sender Filtering**:
22
+ - `const orderPlaced = defineSignal('order_placed')`
23
+ - `orderPlaced.connect(receiver, { sender: 'Checkout' })`
24
+ - `orderPlaced.send({ orderId: 101 }, { sender: 'Checkout' })`
25
+ - **Background Queue Bridging**:
26
+ - `events.listenAndQueue('user.created', 'send-welcome-email', { delay: 1000 })`
27
+ - Automatically bridges emitted events directly to Webspresso's job queue (`core/queue`).
28
+ - **ORM Model Signals Auto-Bridge**:
29
+ - Automatically translates `ModelEvents` (`User.afterCreate`, `*.afterDelete`) to application events (`model:created`, `user:created`, `model:deleted`) with `sender: ctx.model`.
30
+ - **Automatic Request & Context Decoration**:
31
+ - Injected onto `req.events` on every Express request.
32
+ - Available inside `req.context.events` for services: `ctx.events.emit('order.created', order)`.
33
+ - Accessible on `app.events` and plugin contexts.
34
+ - **Graceful Shutdown Integration**:
35
+ - Cleaned up automatically on graceful shutdown via `ShutdownManager.onShutdown()`.
36
+
37
+ ---
38
+
39
+ ## 2. Using Events in API Routes & Services
40
+
41
+ ### 2.1 Emitting Events in File-Based Routes (`pages/api/...`)
42
+
43
+ ```javascript
44
+ // pages/api/orders.post.js
45
+ const { defineApi } = require('webspresso');
46
+
47
+ module.exports = defineApi({
48
+ schema: ({ z }) => ({
49
+ body: z.object({
50
+ total: z.number().positive(),
51
+ items: z.array(z.string()),
52
+ }),
53
+ }),
54
+ async handler(req, res) {
55
+ const order = await req.db.getRepository('Order').create(req.input.body);
56
+
57
+ // Synchronous fire-and-forget or awaited async parallel
58
+ req.events.emit('order.created', order, { sender: 'Order' });
59
+ // or: await req.events.emitAsync('order.created', order);
60
+
61
+ return res.status(201).json(order);
62
+ },
63
+ });
64
+ ```
65
+
66
+ ### 2.2 Emitting Events inside Services (`services/...`)
67
+
68
+ ```javascript
69
+ // services/order/checkout.js
70
+ const { defineService } = require('webspresso');
71
+
72
+ module.exports = defineService({
73
+ transaction: true,
74
+ async handle(input, ctx) {
75
+ const order = await ctx.db.getRepository('Order').create(input);
76
+
77
+ // ctx.events is automatically available
78
+ ctx.events.emit('order.checkout.completed', { orderId: order.id, user: ctx.req?.user });
79
+
80
+ return order;
81
+ },
82
+ });
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 3. Registering Listeners
88
+
89
+ ### 3.1 Programmatic Subscriptions
90
+
91
+ ```javascript
92
+ const { events } = require('webspresso');
93
+
94
+ // 1. Basic Listener
95
+ const unsub = events.on('order.created', (order, meta) => {
96
+ console.log(`Order ${order.id} received at ${meta.timestamp}`);
97
+ });
98
+
99
+ // 2. Wildcard Listener
100
+ events.on('order.*', (data, meta) => {
101
+ console.log(`Order event ${meta.event} occurred`);
102
+ });
103
+
104
+ // 3. One-time Listener
105
+ events.once('app.ready', () => {
106
+ console.log('Application initialized');
107
+ });
108
+
109
+ // 4. Priority Ordering (higher runs first)
110
+ events.on('user.login', auditLogin, { priority: 100 });
111
+ events.on('user.login', notifyFriends, { priority: 0 });
112
+ ```
113
+
114
+ ### 3.2 File-Based Discovery (`listeners/` directory)
115
+
116
+ Webspresso auto-discovers listeners in the `listeners/` root directory:
117
+
118
+ ```javascript
119
+ // listeners/send-welcome-email.js
120
+ const { defineListener } = require('webspresso');
121
+
122
+ module.exports = defineListener('user.created', async (user, meta) => {
123
+ console.log(`Sending welcome email to ${user.email}`);
124
+ });
125
+ ```
126
+
127
+ Or multiple listeners in a single file:
128
+
129
+ ```javascript
130
+ // listeners/audit-events.js
131
+ module.exports = [
132
+ {
133
+ event: 'order.*',
134
+ priority: 50,
135
+ handle: async (data, meta) => {
136
+ console.log(`Audit: ${meta.event}`, data);
137
+ },
138
+ },
139
+ ];
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 4. Django-Style Signals
145
+
146
+ For decoupled domain events with strict sender boundaries:
147
+
148
+ ```javascript
149
+ const { defineSignal } = require('webspresso');
150
+
151
+ // Define signal
152
+ const invoicePaid = defineSignal('invoice_paid');
153
+
154
+ // Connect receiver with sender filter
155
+ invoicePaid.connect(
156
+ (invoice) => {
157
+ console.log(`Invoice ${invoice.id} processed for subscription`);
158
+ },
159
+ { sender: 'Subscription' }
160
+ );
161
+
162
+ // Trigger signal
163
+ invoicePaid.send({ id: 'INV-100' }, { sender: 'Subscription' });
164
+ ```
165
+
166
+ ---
167
+
168
+ ## 5. Background Queue Bridging (`listenAndQueue`)
169
+
170
+ Connect application events directly to background jobs without manual boilerplate:
171
+
172
+ ```javascript
173
+ // In plugin or server setup:
174
+ app.events.listenAndQueue('order.created', 'send-order-confirmation', {
175
+ delay: 2000,
176
+ attempts: 3,
177
+ });
178
+ ```
179
+ When `order.created` is emitted, Webspresso automatically calls `req.queue.dispatch('send-order-confirmation', order, options)`.
package/.agents/ORM.md CHANGED
@@ -400,5 +400,64 @@ await authorRepo.restore(authorId, { cascade: false });
400
400
 
401
401
  If your data model contains circular relations (e.g. `User.partner` <-> `Partner.user`), Webspresso ORM uses an internal visited set (`model:id`) to terminate recursion cleanly without stack overflows.
402
402
 
403
+ ---
404
+
405
+ ## 10. Django-inspired Auto-Migrations (`db:makemigrations`)
406
+
407
+ Webspresso supports automatic schema diffing against your live database, similar to Django's `makemigrations`. It inspects your `models/*.js` definitions, checks the current database schema, and automatically generates required Knex migration files (`createTable` or `alterTable` with `dropColumn` rollbacks):
408
+
409
+ ```bash
410
+ # Preview changes without writing files
411
+ npx webspresso db:makemigrations --dry-run
412
+
413
+ # Detect model changes and generate migration file
414
+ npx webspresso db:makemigrations
415
+
416
+ # Custom migration name
417
+ npx webspresso db:makemigrations --name add_profile_fields
418
+
419
+ # Apply migrations
420
+ npx webspresso db:migrate
421
+ ```
422
+
423
+ Programmatic usage via `db.migrate`:
424
+ ```javascript
425
+ // Check diff
426
+ const diff = await db.migrate.diff();
427
+ if (diff.hasChanges) {
428
+ console.log(diff.changes);
429
+ }
430
+
431
+ // Generate files
432
+ const result = await db.migrate.makeMigrations();
433
+ ```
434
+
435
+ ---
436
+
437
+ ## 11. AdonisJS-inspired Model Factories (`defineFactory`)
438
+
439
+ For unit testing, integration tests, and dev database seeding, Webspresso provides declarative Model Factories inspired by AdonisJS Lucid:
440
+
441
+ ```javascript
442
+ import { defineFactory } from 'webspresso';
443
+
444
+ export const UserFactory = defineFactory('User', ({ faker, index }) => ({
445
+ name: faker.person.fullName(),
446
+ email: faker.internet.email(),
447
+ role: 'user',
448
+ isActive: true,
449
+ }))
450
+ .state('admin', () => ({ role: 'admin' }))
451
+ .state('suspended', () => ({ isActive: false }));
452
+
453
+ // In-memory object generation (No DB hit)
454
+ const fakeUser = UserFactory.make({ name: 'Custom Name' });
455
+ const fakeUsers = UserFactory.count(5).make();
456
+
457
+ // Database persistence via Repository
458
+ const user = await UserFactory.create({ db });
459
+ const admins = await UserFactory.count(3).apply('admin').create({ db });
460
+ ```
461
+
403
462
 
404
463