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.
- package/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +2 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/ROUTING.md +64 -1
- package/AGENTS.md +1 -0
- package/bin/commands/db-makemigrations.js +83 -0
- package/bin/commands/list.js +96 -0
- package/bin/commands/make-command.js +93 -0
- package/bin/commands/make-event.js +80 -0
- package/bin/commands/make-factory.js +72 -0
- package/bin/commands/make-mail.js +93 -0
- package/bin/commands/make-policy.js +106 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/auth/bouncer.js +196 -0
- package/core/auth/index.js +10 -0
- package/core/auth/middleware.js +15 -1
- package/core/commands/define-command.js +48 -0
- package/core/commands/index.js +18 -0
- package/core/commands/registry.js +156 -0
- package/core/commands/runner.js +76 -0
- package/core/compression/compressible.js +1 -1
- package/core/compression/middleware.js +35 -251
- package/core/content/service.js +8 -2
- package/core/events/discovery.js +142 -0
- package/core/events/dispatcher.js +552 -0
- package/core/events/index.js +23 -0
- package/core/events/signal.js +92 -0
- package/core/forms/flash.js +234 -0
- package/core/forms/form-builder.js +214 -0
- package/core/forms/index.js +16 -0
- package/core/mail/index.js +23 -0
- package/core/mail/mailable.js +250 -0
- package/core/mail/mailer.js +184 -0
- package/core/mail/transports.js +238 -0
- package/core/orm/factory.js +250 -0
- package/core/orm/index.js +6 -0
- package/core/orm/migrations/diff.js +185 -0
- package/core/orm/migrations/index.js +22 -4
- package/core/orm/model.js +4 -0
- package/index.d.ts +266 -0
- package/index.js +40 -0
- package/package.json +5 -1
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +17 -0
- package/plugins/content/models/content-entry.js +4 -3
- package/plugins/content/models/content-type.js +4 -3
- package/plugins/cors.js +2 -2
- package/plugins/email/index.js +48 -0
- package/plugins/email/preview-handler.js +555 -0
- package/plugins/email/service.js +23 -12
- package/plugins/email/transport.js +30 -11
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +1 -1
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/scan-routes.js +10 -1
- package/src/helpers.js +143 -0
- package/src/pages/page-loader.js +1 -1
- package/src/routing/cascading.js +14 -10
- 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
|
-
##
|
|
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
|
-
##
|
|
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
|
|
package/.agents/ROUTING.md
CHANGED
|
@@ -435,7 +435,70 @@ In SSR streaming mode (`stream: true` or `renderStream`):
|
|
|
435
435
|
|
|
436
436
|
---
|
|
437
437
|
|
|
438
|
-
## 7.
|
|
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
|
---
|