webspresso 0.0.100 → 0.0.101

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 (65) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +2 -0
  3. package/.agents/AUTH.md +63 -0
  4. package/.agents/CLI.md +81 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/EVENTS.md +179 -0
  7. package/.agents/ORM.md +59 -0
  8. package/.agents/ROUTING.md +64 -1
  9. package/AGENTS.md +1 -0
  10. package/bin/commands/db-makemigrations.js +83 -0
  11. package/bin/commands/list.js +96 -0
  12. package/bin/commands/make-command.js +93 -0
  13. package/bin/commands/make-event.js +80 -0
  14. package/bin/commands/make-factory.js +72 -0
  15. package/bin/commands/make-mail.js +93 -0
  16. package/bin/commands/make-policy.js +106 -0
  17. package/bin/commands/run.js +195 -0
  18. package/bin/webspresso.js +17 -0
  19. package/core/auth/bouncer.js +196 -0
  20. package/core/auth/index.js +10 -0
  21. package/core/auth/middleware.js +15 -1
  22. package/core/commands/define-command.js +48 -0
  23. package/core/commands/index.js +18 -0
  24. package/core/commands/registry.js +156 -0
  25. package/core/commands/runner.js +76 -0
  26. package/core/compression/compressible.js +1 -1
  27. package/core/compression/middleware.js +35 -251
  28. package/core/content/service.js +8 -2
  29. package/core/events/discovery.js +142 -0
  30. package/core/events/dispatcher.js +552 -0
  31. package/core/events/index.js +23 -0
  32. package/core/events/signal.js +92 -0
  33. package/core/forms/flash.js +234 -0
  34. package/core/forms/form-builder.js +214 -0
  35. package/core/forms/index.js +16 -0
  36. package/core/mail/index.js +23 -0
  37. package/core/mail/mailable.js +250 -0
  38. package/core/mail/mailer.js +184 -0
  39. package/core/mail/transports.js +238 -0
  40. package/core/orm/factory.js +250 -0
  41. package/core/orm/index.js +6 -0
  42. package/core/orm/migrations/diff.js +185 -0
  43. package/core/orm/migrations/index.js +22 -4
  44. package/core/orm/model.js +4 -0
  45. package/index.d.ts +266 -0
  46. package/index.js +40 -0
  47. package/package.json +5 -1
  48. package/plugins/admin-panel/api.js +509 -3
  49. package/plugins/admin-panel/index.js +17 -0
  50. package/plugins/content/models/content-entry.js +4 -3
  51. package/plugins/content/models/content-type.js +4 -3
  52. package/plugins/cors.js +2 -2
  53. package/plugins/email/index.js +48 -0
  54. package/plugins/email/preview-handler.js +555 -0
  55. package/plugins/email/service.js +23 -12
  56. package/plugins/email/transport.js +30 -11
  57. package/plugins/redirect/index.js +1 -1
  58. package/src/api/api-loader.js +1 -1
  59. package/src/app-context.js +48 -6
  60. package/src/dev-reload/index.js +9 -1
  61. package/src/discovery/scan-routes.js +10 -1
  62. package/src/helpers.js +143 -0
  63. package/src/pages/page-loader.js +1 -1
  64. package/src/routing/cascading.js +14 -10
  65. package/src/server.js +180 -26
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
  ---
@@ -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
 
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
+
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
 
@@ -435,7 +435,70 @@ In SSR streaming mode (`stream: true` or `renderStream`):
435
435
 
436
436
  ---
437
437
 
438
- ## 7. Summary Checklist for Agents
438
+ ## 7. Form Handling, Flash Messages & Old Input (AdonisJS & Django-inspired)
439
+
440
+ For classic SSR forms (POST-Redirect-GET), Webspresso provides built-in flash data propagation and Django-style ModelForm rendering helpers.
441
+
442
+ ### 7.1 Redirect with Old Input & Validation Errors
443
+ In your form submission handler (API or custom POST action):
444
+ ```javascript
445
+ // pages/api/auth/register.post.js
446
+ module.exports = {
447
+ handler: async (req, res) => {
448
+ const { email, password } = req.body;
449
+
450
+ if (!email.includes('@')) {
451
+ return res
452
+ .withInput() // Preserves req.body (sanitizes sensitive fields like password)
453
+ .withErrors({ email: 'Please enter a valid email address' })
454
+ .withFlash('warning', 'Please review the highlighted errors')
455
+ .redirect('/register');
456
+ }
457
+
458
+ await req.service('user.create', { email, password });
459
+ return res.withFlash('success', 'Account created! Please log in.').redirect('/login');
460
+ }
461
+ };
462
+ ```
463
+
464
+ ### 7.2 Nunjucks Template Helpers (`fsy`)
465
+ In your Nunjucks templates (`pages/*.njk`):
466
+ ```html
467
+ {% if fsy.hasFlash('success') %}
468
+ <div class="alert alert-success">{{ fsy.flash('success') }}</div>
469
+ {% endif %}
470
+
471
+ <form method="POST" action="/api/auth/register">
472
+ {{ fsy.csrf() | safe }}
473
+
474
+ <!-- Manual field with old value and inline error -->
475
+ <div class="mb-3">
476
+ <label class="form-label">Email</label>
477
+ <input type="email" name="email"
478
+ value="{{ fsy.old('email') }}"
479
+ class="form-control {% if fsy.hasError('email') %}is-invalid{% endif %}">
480
+ {% if fsy.hasError('email') %}
481
+ <div class="invalid-feedback">{{ fsy.error('email') }}</div>
482
+ {% endif %}
483
+ </div>
484
+
485
+ <!-- Or use the Django-inspired field helper (one-liner) -->
486
+ {{ fsy.field('email', { type: 'email', placeholder: 'you@example.com' }) | safe }}
487
+
488
+ <button type="submit" class="btn btn-primary">Register</button>
489
+ </form>
490
+ ```
491
+
492
+ ### 7.3 Django ModelForm Helper (`fsy.formFor`)
493
+ Generate an entire HTML form from an ORM model definition with automatic widgets and validation error mapping:
494
+ ```html
495
+ {# Automatically excludes id, created_at, updated_at and generates appropriate widgets #}
496
+ {{ fsy.formFor(req.db.getRepository('Article'), { action: '/articles', submitLabel: 'Publish' }) | safe }}
497
+ ```
498
+
499
+ ---
500
+
501
+ ## 8. Summary Checklist for Agents
439
502
 
440
503
  When a user asks to add or modify endpoints:
441
504
  1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).
package/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`](.agents/ADMIN.md) | `plugins/admin-panel/` |
18
18
  | **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](.agents/AUTH.md) | `core/auth/` |
19
19
  | **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](.agents/QUEUE.md) | `jobs/` |
20
+ | **Decoupled Events & Signals** | [`.agents/EVENTS.md`](.agents/EVENTS.md) | `core/events/`, `listeners/` |
20
21
  | **API Signatures & Types Verification** | [`index.d.ts`](index.d.ts) | **Canonical Ground Truth** |
21
22
 
22
23
  ---