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.
- package/.agents/ADMIN.md +47 -4
- package/.agents/AGENTS.md +3 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/AUTH.md +63 -0
- package/.agents/CLI.md +81 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/EMAIL.md +84 -2
- package/.agents/EVENTS.md +179 -0
- package/.agents/ORM.md +59 -0
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +163 -3
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +7 -0
- package/README.md +18 -5
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -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/routes.js +8 -0
- package/bin/commands/run.js +195 -0
- package/bin/webspresso.js +17 -0
- package/core/applySchema.js +18 -1
- 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 +422 -1
- package/index.js +111 -16
- package/package.json +9 -5
- package/plugins/admin-panel/api.js +509 -3
- package/plugins/admin-panel/index.js +29 -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/csrf/index.js +1 -1
- 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/file-manager/index.js +13 -0
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/redirect/index.js +1 -1
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/app-context.js +48 -6
- package/src/dev-reload/index.js +9 -1
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +80 -5
- package/src/file-router.js +153 -91
- package/src/helpers.js +143 -0
- package/src/njk-frontmatter.js +17 -1
- package/src/pages/page-loader.js +1 -1
- package/src/plugin-manager.js +12 -0
- package/src/routing/cascading.js +14 -10
- package/src/routing/route-table.js +1 -1
- package/src/server.js +243 -34
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- 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
|
|
package/.agents/ARCHITECTURE.md
CHANGED
|
@@ -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
|
+
|
package/.agents/CONVENTIONS.md
CHANGED
|
@@ -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
|
-
##
|
|
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
|
|