webspresso 0.0.90 → 0.0.93

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 (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
package/README.md CHANGED
@@ -2,2565 +2,154 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/webspresso.svg?style=flat-square)](https://www.npmjs.com/package/webspresso)
4
4
  [![vulnerabilities](https://npmx.dev/api/registry/badge/vulnerabilities/webspresso?style=shieldsio)](https://npmx.dev/package/webspresso)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
5
6
 
6
- A minimal, file-based SSR framework for Node.js with Nunjucks templating.
7
+ A lightweight, zero-dependency-sprawl Express SSR framework featuring file-based routing, server-side Nunjucks rendering, an intuitive Knex-based ORM with ambient `AsyncLocalStorage` transactions, dual authentication (Session + JWT), zero-dependency streaming compression, graceful shutdown, and a customizable Mithril.js SPA Admin Panel.
7
8
 
8
- ## Features
9
+ ---
9
10
 
10
- - **File-Based Routing**: Create pages by adding `.njk` files to a `pages/` directory
11
- - **Dynamic Routes**: Use `[param]` for dynamic params and `[...rest]` for catch-all routes
12
- - **API Endpoints**: Add `.js` files to `pages/api/` with method suffixes (e.g., `health.get.js`)
13
- - **Schema Validation**: Zod-based request validation for body, params, and query
14
- - **Built-in i18n**: JSON-based translations with automatic locale detection
15
- - **Lifecycle Hooks**: Global and route-level hooks for request processing
16
- - **Template Helpers**: Laravel-inspired helper functions available in templates
17
- - **Plugin System**: Extensible architecture with version control and inter-plugin communication
18
- - **Built-in Plugins**: Development dashboard, sitemap generator, SEO checker, analytics integration (Google, Yandex, Bing), self-hosted site analytics, optional Swagger UI for HTTP APIs, configurable HTTP health probe endpoint, optional REST CRUD routes from ORM models, optional admin UI for ORM query cache metrics and purge, optional **admin-only spreadsheet exchange** (Excel export, CSV/XLSX import via `dataExchangePlugin`)
19
- - **Session authentication** (optional): `createAuth` / `quickAuth` in **`webspresso/core/auth`** — pass the manager to **`createApp({ auth })`** for `express-session`, `req.user` / `req.auth`, remember-me tokens, and policy-style authorization. Full walkthrough: **[`doc/index.html#authentication`](doc/index.html#authentication)**.
20
- - **Optional client runtime** (Alpine.js + [swup](https://swup.js.org/)): **`createApp({ clientRuntime: { alpine, swup } })`** serves scripts under **`/__webspresso/client-runtime/`** and exposes **`clientRuntime`** in Nunjucks; layouts can include **`views/partials/webspresso-client-runtime.njk`**. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`**. Demo: **`examples/alpine-swup-demo/`**. Details: **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
21
- - **TypeScript**: Published **`index.d.ts`** (via `package.json` `"types"`) for `createApp`, ORM, plugins, and router helpers — use from TS/JS with IDE autocomplete; runtime stays CommonJS
22
- - **Application kernel (optional)**: In-process **`kernel`** API (`require('webspresso').kernel`) — event bus (`dispatch` / `publish`), **`kernel.createApp()`** (namespaced differently from SSR **`createApp`**), **`definePlugin`** / **`defineFlow`**, minimal **`{{ }}` view resolver**, and simulated **`BaseRepository`** with `orm.<resource>.*` events. Ships as **`core/kernel/`** on npm. Demo: **`node core/kernel/run-demo.js`**. Docs: **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)**.
11
+ ## ⚡ Quick Start (3 Minutes)
23
12
 
24
- ## Installation
13
+ Scaffold a new project with interactive database and styling setup:
25
14
 
26
15
  ```bash
27
- npm install -g webspresso
28
- # or
29
- npm install webspresso
30
- ```
31
-
32
- ## TypeScript
33
-
34
- The npm package ships with **[`index.d.ts`](index.d.ts)** so consumers get typings for the public API (`createApp`, `defineModel`, `createDatabase`, `zdb`, plugins, etc.). No extra `@types/webspresso` package is required.
35
-
36
- ```typescript
37
- import { createApp, defineModel, zdb } from 'webspresso';
38
- ```
39
-
40
- Install **`@types/express`** in your app if you want full **`Express.Application`** / **`Request`** / **`Response`** inference when you touch `createApp().app` or write middleware. **`knex`** and **`zod`** bring their own types.
41
-
42
- Framework development (this repo): run **`npm run check:types`** to typecheck the declarations against a small smoke file (`tests/ts-smoke/`).
43
-
44
- ## Application kernel (`kernel`)
45
-
46
- Do **not** confuse **`kernel.createApp()`** with the package root **`createApp`** used for SSR—it returns a different object (event bus, optional flows, and a minimal view resolver). It does **not** modify Knex ORM behavior or Express routing.
47
-
48
- ```javascript
49
- const { kernel } = require('webspresso');
50
- const app = kernel.createApp();
51
- app.events.on('orm.post.afterCreate', async (ctx) => { /* ... */ });
52
- ```
53
-
54
- See **[`doc/index.html#application-kernel`](doc/index.html#application-kernel)** · source modules: [`core/kernel/`](core/kernel/). Cursor skill: **`REFERENCE-kernel.md`** (installed via `webspresso skill --preset webspresso`).
55
-
56
- ## Quick Start
57
-
58
- ### Using CLI (Recommended)
59
-
60
- ```bash
61
- # Create a new project (Tailwind CSS included by default)
62
- webspresso new my-app
16
+ # Create a new project interactively
17
+ npx webspresso new my-app --install
63
18
 
64
- # Navigate to project
19
+ # Navigate and start development server
65
20
  cd my-app
66
-
67
- # Install dependencies
68
- npm install
69
-
70
- # Build Tailwind CSS
71
- npm run build:css
72
-
73
- # Start development server (watches both CSS and server)
74
- webspresso dev
75
- # or
76
21
  npm run dev
77
22
  ```
78
23
 
79
- > **Note:** New projects include Tailwind CSS by default. Use `--no-tailwind` flag to skip it.
80
-
81
- ## CLI Commands
82
-
83
- ### `webspresso new [project-name]`
84
-
85
- Create a new Webspresso project with Tailwind CSS (default).
86
-
87
- ```bash
88
- # Create in a new directory
89
- webspresso new my-app
90
-
91
- # Create in current directory (interactive)
92
- webspresso new
93
- # → Prompts: "Install in current directory?"
94
- # → If yes, asks for project name (for package.json)
95
-
96
- # Auto install dependencies and build CSS
97
- webspresso new my-app --install
98
-
99
- # Without Tailwind
100
- webspresso new my-app --no-tailwind
101
- ```
102
-
103
- **Interactive Mode (no arguments):**
104
- - Asks if you want to install in the current directory
105
- - If current directory is not empty, shows a warning
106
- - Prompts for project name (defaults to current folder name)
107
- - Asks if you will use a database (SQLite, PostgreSQL, or MySQL)
108
- - If yes, adds the appropriate driver to `package.json` and creates `webspresso.db.js` config
109
- - After project creation, asks if you want to install dependencies
110
- - If yes, runs `npm install` and `npm run build:css`
111
- - Then asks if you want to start the development server
112
- - If yes, starts `npm run dev` automatically
113
-
114
- **Auto Installation:**
115
- ```bash
116
- # With --install flag (semi-interactive)
117
- webspresso new my-app --install
118
- # → Automatically runs: npm install && npm run build:css
119
- # → Then prompts: "Start development server?" [Y/n]
120
- # → If yes: starts npm run dev (with watch:css if Tailwind enabled)
121
-
122
- # Without --install flag (fully interactive)
123
- webspresso new my-app
124
- # → Prompts: "Install dependencies and build CSS now?" [Y/n]
125
- # → If yes: runs npm install && npm run build:css
126
- # → Then: "Start development server?" [Y/n]
127
- # → If yes: starts npm run dev (with watch:css if Tailwind enabled)
128
- ```
129
-
130
- **Note:** When dev server starts with Tailwind CSS, it automatically runs `watch:css` in the background to watch for CSS changes.
131
-
132
- **Database Selection:**
133
- During project creation, you'll be asked if you want to use a database:
134
- - **SQLite** (better-sqlite3) - Recommended for development and small projects
135
- - **PostgreSQL** (pg) - For production applications
136
- - **MySQL** (mysql2) - Alternative SQL database
137
-
138
- If you select a database:
139
- - The appropriate driver is added to `package.json` dependencies
140
- - `webspresso.db.js` config file is created with proper settings
141
- - `migrations/` directory is created
142
- - `models/` directory is created
143
- - `DATABASE_URL` is added to `.env.example` with a template
144
-
145
- **Scaffold: `config/` and environment files**
146
-
147
- New projects include:
148
-
149
- - **`config/load-env.js`** — loads, in order, `.env`, `.env.local`, `.env.${NODE_ENV}`, and `.env.${NODE_ENV}.local` (each file overrides keys from earlier ones).
150
- - **`config/env.schema.js`** — validates `process.env` with **Zod** before the app starts (`PORT`, `NODE_ENV`, i18n vars, `BASE_URL`, optional `DATABASE_URL`).
151
- - **`config/app.js`** — returns `createApp()` options (paths; if `webspresso.db.js` exists, also **`createDatabase`** as `db`).
152
- - **`server.js`** — calls `loadEnv()`, then `createApp(getCreateAppOptions())`, then `listen` using the parsed `PORT`.
153
-
154
- Copy **`.env.example`** to **`.env`** (and use `.env.local` for machine-specific overrides). Patterns such as `.env.development.local` are gitignored via `.env.*.local`.
155
-
156
- **Seed Data Generation:**
157
- After selecting a database, you'll be asked if you want to generate seed data:
158
- - If yes, `@faker-js/faker` is added to dependencies
159
- - `seeds/` directory is created with `seeds/index.js`
160
- - `npm run seed` script is added to `package.json`
161
- - The seed script automatically detects models in `models/` directory and generates fake data based on their schemas
162
-
163
- To run seeds after creating models:
164
- ```bash
165
- npm run seed
166
- ```
167
-
168
- The seed script will:
169
- - Load all models from `models/` directory
170
- - Generate 10 fake records per model (by default)
171
- - Use smart field detection based on column names (email, name, title, etc.)
172
-
173
- You can always add database support later by:
174
- 1. Installing the driver: `npm install better-sqlite3` (or `pg`, `mysql2`)
175
- 2. Creating `webspresso.db.js` config file
176
- 3. Adding `DATABASE_URL` to your `.env` file
177
- 4. Creating `models/` directory and defining your models
178
- 5. Optionally adding seed support: `npm install @faker-js/faker` and creating `seeds/index.js`
179
-
180
- Options:
181
- - `-i, --install` - Auto run `npm install` and `npm run build:css` (non-interactive)
182
- - `--no-tailwind` - Skip Tailwind CSS setup
183
-
184
- The project includes:
185
- - Tailwind CSS with build process
186
- - Optimized layout template with navigation and footer
187
- - Responsive starter page
188
- - i18n setup (e.g. en/de)
189
- - Development and production scripts
190
-
191
- ### `webspresso page`
192
-
193
- Add a new page to your project (interactive prompt).
194
-
195
- ```bash
196
- webspresso page
197
- ```
198
-
199
- The CLI will ask you:
200
- - Route path (e.g., `/about` or `/blog/post`)
201
- - Whether to add a route config file
202
- - Whether to add locale files
203
-
204
- ### `webspresso api`
205
-
206
- Add a new API endpoint (interactive prompt).
207
-
208
- ```bash
209
- webspresso api
210
- ```
211
-
212
- The CLI will ask you:
213
- - API route path (e.g., `/api/users` or `/api/users/[id]`)
214
- - HTTP method (GET, POST, PUT, PATCH, DELETE)
215
-
216
- ### `webspresso dev`
217
-
218
- Start development server with hot reload.
219
-
220
- ```bash
221
- webspresso dev
222
- # or with custom port
223
- webspresso dev --port 3001
224
- ```
225
-
226
- ### `webspresso start`
227
-
228
- Start production server.
229
-
230
- ```bash
231
- webspresso start
232
- # or with custom port
233
- webspresso start --port 3000
234
- ```
235
-
236
- ### `webspresso doctor`
237
-
238
- Check Node.js version, `package.json` / `engines.node`, typical project files (`server.js`, `pages/`), and whether `webspresso.db.js` or `knexfile.js` exists. Use `--db` to run a quick connection test when a config is present. Warnings alone exit with code `0`; pass `--strict` to fail (exit `1`) on any warning—useful in CI.
239
-
240
- ```bash
241
- webspresso doctor
242
- webspresso doctor --db
243
- webspresso doctor --strict
244
- ```
245
-
246
- ### `webspresso skill`
247
-
248
- Scaffold an **Agent Skill**: creates `.agents/skills/<name>/SKILL.md` with valid YAML frontmatter (`name`, `description`) and a short markdown template for AI tooling. Use `--global` to write under `~/.agents/skills/` instead of the current project.
249
-
250
- **Bundled preset:** `--preset webspresso` copies **`SKILL.md`** (short index), **`REFERENCE-framework.md`** (SSR `createApp`, routes, ORM, auth, plugins, CLI), and **`REFERENCE-kernel.md`** (`kernel` event bus / flows) into **`.agents/skills/webspresso-usage/`** — no prompts.
251
-
252
-
253
- ```bash
254
- webspresso skill my-workflow
255
- webspresso skill review-pr --description "Runs PR review checklist. Use when reviewing pull requests."
256
- webspresso skill deploy-check -g
257
-
258
- # Install the bundled Webspresso agent reference (same content shipped in templates/skills/)
259
- webspresso skill --preset webspresso
260
- webspresso skill -p webspresso --global
261
- ```
262
-
263
- ### `webspresso add tailwind`
264
-
265
- Add Tailwind CSS to your project with build process.
266
-
267
- ```bash
268
- webspresso add tailwind
269
- ```
270
-
271
- This command will:
272
- - Install Tailwind CSS, PostCSS, and Autoprefixer as dev dependencies
273
- - Create `tailwind.config.js` and `postcss.config.js`
274
- - Create `src/input.css` with Tailwind directives
275
- - Add build scripts to `package.json`
276
- - Update your layout to use the built CSS instead of CDN
277
- - Create `public/css/style.css` for the compiled output
278
-
279
- After running this command:
280
- ```bash
281
- npm install
282
- npm run build:css # Build CSS once
283
- npm run watch:css # Watch and rebuild CSS on changes
284
- npm run dev # Starts both CSS watch and dev server
285
- ```
286
-
287
- ### `webspresso favicon:generate <source.png>`
288
-
289
- Generate favicon PNG files and `favicons.njk` partial from a single source PNG.
290
-
291
- ```bash
292
- # Basic usage (creates files in public/, views/partials/)
293
- webspresso favicon:generate logo.png
294
-
295
- # With PWA manifest options
296
- webspresso favicon:generate logo.png --name "My App" --short-name "App" --theme-color "#22c55e"
297
-
298
- # Custom output directory
299
- webspresso favicon:generate logo.png -o static
300
-
301
- # Skip adding include to layout.njk
302
- webspresso favicon:generate logo.png --no-layout
303
- ```
304
-
305
- This command will:
306
- - Resize the source PNG to all required sizes (Apple touch 57–180px, Android 192px, favicon 16/32/96px, MS Tile 144px)
307
- - Write PNGs to `public/` (or `-o` path)
308
- - Create `public/manifest.json` (PWA format)
309
- - Create `views/partials/favicons.njk` with `<link>` and `<meta>` tags
310
- - Add `{% include "partials/favicons.njk" %}` to `views/layout.njk` (unless `--no-layout`)
311
-
312
- **Options:**
313
- - `-o, --output-dir <path>` – Output directory for PNGs (default: `public`)
314
- - `--partial-dir <path>` – Directory for favicons.njk (default: `views/partials`)
315
- - `--layout-file <path>` – Layout file to update (default: `views/layout.njk`)
316
- - `--theme-color <hex>` – theme-color and msapplication-TileColor (default: `#ffffff`)
317
- - `--name <string>` – manifest.json `name` (PWA)
318
- - `--short-name <string>` – manifest.json `short_name` (PWA)
319
- - `--no-layout` – Do not add include to layout.njk
320
-
321
- ### Admin Panel Commands
322
-
323
- ```bash
324
- # Create admin_users table migration
325
- webspresso admin:setup
326
-
327
- # List all admin users
328
- webspresso admin:list
329
-
330
- # Reset admin password (interactive)
331
- webspresso admin:password
332
-
333
- # Reset with options
334
- webspresso admin:password -e admin@example.com -p yeni_sifre123
335
- webspresso admin:password -c ./webspresso.db.js -E production
336
- ```
337
-
338
- > **Note:** Requires `webspresso.db.js` or `knexfile.js` in project root. Run from project directory.
339
-
340
- ## Project Structure
341
-
342
- Create your app with this structure:
343
-
344
- ```
345
- my-app/
346
- ├── pages/
347
- │ ├── locales/ # Global i18n translations
348
- │ │ ├── en.json
349
- │ │ └── de.json
350
- │ ├── _hooks.js # Global lifecycle hooks
351
- │ ├── index.njk # Home page (GET /)
352
- │ ├── about/
353
- │ │ ├── index.njk # About page (GET /about)
354
- │ │ └── locales/ # Route-specific translations
355
- │ ├── tools/
356
- │ │ ├── index.njk # Tools list (GET /tools)
357
- │ │ ├── index.js # Route config with load()
358
- │ │ ├── [slug].njk # Dynamic tool page (GET /tools/:slug)
359
- │ │ └── [slug].js # Route config for dynamic page
360
- │ └── api/
361
- │ ├── health.get.js # GET /api/health
362
- │ └── echo.post.js # POST /api/echo
363
- ├── views/
364
- │ └── layout.njk # Base layout template
365
- ├── public/ # Static files
366
- └── server.js
367
- ```
368
-
369
- ## API
370
-
371
- ### `createApp(options)`
372
-
373
- Creates and configures the Express app.
374
-
375
- **Options:**
376
- - `pagesDir` (required): Path to pages directory
377
- - `viewsDir` (optional): Path to views/layouts directory
378
- - `publicDir` (optional): Path to public/static directory
379
- - `db` (optional): Database instance — exposed as `ctx.db` in plugin hooks (`register`, `onRoutesReady`) and in page `load`/`meta` functions; also registered for [`getDb()` / `getAppContext()`](#app-context-getdb-hasdb-getappcontext) below
380
- - `logging` (optional): Enable request logging (default: true in development)
381
- - `helmet` (optional): Helmet security configuration
382
- - `true` or `undefined`: Use default secure configuration
383
- - `false`: Disable Helmet
384
- - `Object`: Custom Helmet configuration (merged with defaults)
385
- - `middlewares` (optional): Named middleware registry for routes
386
- - `clientRuntime` (optional): **`{ alpine?: boolean | object, swup?: boolean | object }`**. When either flag is enabled, mounts vendored Alpine 3 / swup 4 (+ Head + Scripts plugins) at **`/__webspresso/client-runtime/*`** and passes resolved **`{ alpine, swup }`** into SSR templates as **`clientRuntime`**. Override with env **`WEBSPRESSO_ALPINE`** / **`WEBSPRESSO_SWUP`** (`1` or `true`). Package exports **`resolveClientRuntime`** and **`CLIENT_RUNTIME_BASE`**. See **[Client runtime](#client-runtime)** below and **[`doc/index.html#client-runtime`](doc/index.html#client-runtime)**.
387
- - `auth` (optional): `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`require('webspresso/core/auth')`). Registers session + cookie parsing, attaches **`req.auth`** / **`req.user`**, and injects named route middleware **`auth`** and **`guest`** (do not reuse those names for custom handlers if you pass `auth`). See **[`doc/index.html#authentication`](doc/index.html#authentication)**.
388
- - `setupRoutes(app, ctx)` (optional): Register custom Express routes after file routes and plugin `onRoutesReady`, before the 404 handler — use for login/logout handlers when using the auth module; **`ctx.authMiddleware`** exposes `requireAuth`, `requireGuest`, etc.; **`ctx.clientRuntime`** is **`{ alpine, swup }`**
24
+ Open **`http://localhost:3000`** in your browser.
389
25
 
390
- **Example with middlewares:**
26
+ ---
391
27
 
392
- ```javascript
393
- const { createApp } = require('webspresso');
394
-
395
- const { app } = createApp({
396
- pagesDir: './pages',
397
- viewsDir: './views',
398
- middlewares: {
399
- auth: (req, res, next) => {
400
- if (!req.session?.user) {
401
- return res.redirect('/login');
402
- }
403
- next();
404
- },
405
- admin: (req, res, next) => {
406
- if (req.session?.user?.role !== 'admin') {
407
- return res.status(403).send('Forbidden');
408
- }
409
- next();
410
- },
411
- rateLimit: require('express-rate-limit')({ windowMs: 60000, max: 100 })
412
- }
413
- });
414
- ```
415
-
416
- Then use in route configs by name:
28
+ ## ✨ Why Webspresso?
417
29
 
418
- ```javascript
419
- // pages/admin/index.js
420
- module.exports = {
421
- middleware: ['auth', 'admin'], // Use named middlewares
422
- load(req, ctx) { ... }
423
- };
30
+ - **Zero-Dependency Sprawl**: Core framework features (Dual Auth, JWT signing, Error handling, Streaming Compression, Graceful Shutdown, Template Helpers) rely strictly on native Node.js standard modules (`crypto`, `path`, `fs`, `events`, `zlib`, `async_hooks`) without bloated dependency trees.
31
+ - **File-Based Routing**: Templates in `pages/` automatically become public URL routes with support for dynamic params (`[id]`) and catch-all wildcards (`[...slug]`).
32
+ - **Server Data Prefetching (`load()`)**: Page routes export `async function load({ req, res, db, ctx })` to fetch data server-side before template compilation.
33
+ - **Services Layer**: File-based auto-discovery in `services/` with declarative Zod validation, role/predicate authorization guards, and memoization.
34
+ - **Ambient Transactions**: Knex database transactions automatically bind to `AsyncLocalStorage` (`core/orm/transaction.js`). Scoped repositories seamlessly participate in active transactions without manual `trx` passing.
35
+ - **Mithril.js Admin Panel SPA**: Built-in, isolated single-page admin panel mounted at `/_admin` with auto-layout wrapping for custom pages, widgets, and field renderers.
36
+ - **TypeScript First-Class Support**: Shipped with complete [`index.d.ts`](index.d.ts) type definitions for IDE autocompletion and type checking.
424
37
 
425
- // pages/api/data.get.js
426
- module.exports = {
427
- middleware: ['auth', 'rateLimit'],
428
- handler: (req, res) => res.json({ data: 'protected' })
429
- };
430
- ```
38
+ ---
431
39
 
432
- **Parameterized named middleware:** entries in `middlewares` can be **factories** `(options) => (req, res, next) => …`. A bare string calls the factory with `{}`; a **tuple** passes options:
40
+ ## 🛠️ Minimal Example
433
41
 
42
+ ### 1. Server Entry Point (`server.js`)
434
43
  ```javascript
435
- // pages/api/account.get.js
436
- module.exports = {
437
- middleware: [['auth', { api: true }], 'rateLimit'], // JSON 401 instead of redirect when using createApp({ auth })
438
- handler: (req, res) => res.json({ ok: true }),
439
- };
440
-
441
- // server.js — custom factory
442
- middlewares: {
443
- oauth: (opts) => (req, res, next) => {
444
- if (opts.google && !req.headers['x-google']) return res.status(401).end();
445
- next();
446
- },
447
- };
448
- // pages/example/index.js → middleware: [['oauth', { google: true }], 'auth']
449
- ```
450
-
451
- Plain `(req, res, next) => …` handlers still work as today. Tuple form **requires** a factory for that name (you get a clear error if you mix a plain handler with `['name', opts]`).
452
-
453
- ### Client runtime
454
-
455
- Alpine.js + swup — opt-in progressive enhancement for SSR pages:
456
-
457
- - **`clientRuntime: { alpine: true, swup: true }`** (each can be toggled independently). Default is off; no scripts are injected when both are disabled.
458
- - Include **`{% include "partials/webspresso-client-runtime.njk" %}`** in your layout (copy from the npm package’s **`views/partials/`** or the framework repo). When **swup** is on, wrap the main content in **`<main id="swup">…</main>`** so transitions replace the correct region.
459
- - **swup** uses Head + Scripts plugins; **Alpine** is re-bound after each visit via **`Alpine.initTree`** on the container. Use **`data-no-swup`** on links for a full page load. Paths **`/_admin`** and **`/_webspresso`** are ignored by the default bootstrap; the admin panel and dev dashboard stay separate Mithril apps.
460
- - Dynamic UI can call **`pages/api/*`** from Alpine with **`fetch`** (see **`examples/alpine-swup-demo/`**).
461
- - **Helmet / CSP**: production **`script-src 'self'`** works for **`/__webspresso/client-runtime/`**; some Alpine builds may need **`unsafe-eval`** — validate for your version or use a stricter build.
44
+ const path = require('path');
45
+ require('./config/load-env');
46
+ const { createApp, createDatabase } = require('webspresso');
462
47
 
463
- ```javascript
464
- const { createApp } = require('webspresso');
48
+ const db = createDatabase(require('./webspresso.db'));
465
49
 
466
50
  const { app } = createApp({
467
- pagesDir: './pages',
468
- viewsDir: './views',
469
- publicDir: './public',
470
- clientRuntime: { alpine: true, swup: true },
471
- });
472
- ```
473
-
474
- ### App context (`req.db`, `getDb`, `attachDbMiddleware`)
475
-
476
- With **`createApp({ db })`**, file-based **API** routes (`pages/api/*.js`) get the same ORM instance on **`req.db`** before your **`middleware`** array and the handler run — no extra `require` in the handler:
477
-
478
- ```javascript
479
- module.exports = async function handler(req, res) {
480
- if (!req.db) {
481
- return res.status(503).json({ error: 'Database not configured' });
482
- }
483
- const posts = await req.db.getRepository('Post').query().limit(10).list();
484
- res.json(posts);
485
- };
486
-
487
- module.exports.middleware = ['auth']; // can use req.db too
488
- ```
489
-
490
- The framework also registers that instance for **non-request** code (scripts, jobs) and for tests:
491
-
492
- ```javascript
493
- const { getDb, hasDb } = require('webspresso');
494
- // hasDb() / getDb() — getDb() throws if createApp had no db
495
- ```
496
-
497
- For routes you add manually in **`setupRoutes`**, run **`attachDbMiddleware`** early so those handlers get `req.db`:
498
-
499
- ```javascript
500
- const { createApp, attachDbMiddleware } = require('webspresso');
501
-
502
- createApp({
503
- pagesDir: './pages',
504
51
  db,
505
- setupRoutes(app) {
506
- app.use(attachDbMiddleware);
507
- app.get('/custom/api', (req, res) => res.json({ ok: !!req.db }));
52
+ pagesDir: path.join(__dirname, 'pages'),
53
+ viewsDir: path.join(__dirname, 'views'),
54
+ server: {
55
+ shutdown: { enabled: true, timeout: 10000 },
56
+ compression: true,
508
57
  },
509
58
  });
510
- ```
511
-
512
- | Export | Role |
513
- |--------|------|
514
- | `req.db` | Set on each API request when `createApp({ db })` was used (file-based API routes + after `attachDbMiddleware`) |
515
- | `getDb()` | Same instance as `req.db`; **throws** if no `db` was passed to `createApp` |
516
- | `hasDb()` | `true` if `createApp` was given `db` |
517
- | `getAppContext()` | `{ db }` — `db` may be `null` |
518
- | `attachDbMiddleware` | Express middleware to populate `req.db` for non–file-router routes |
519
- | `resetAppContext()` | Clears context (mainly for tests) |
520
- | `setAppContext(partial)` | Low-level merge; normally only `createApp` uses this |
521
-
522
- **Custom Error Pages:**
523
59
 
524
- ```javascript
525
- const { createApp } = require('webspresso');
526
-
527
- const { app } = createApp({
528
- pagesDir: './pages',
529
- viewsDir: './views',
530
- errorPages: {
531
- // 404 Not Found: Template path (relative to viewsDir) or custom handler fn
532
- notFound: '404.njk', // or 'errors/404.njk', or (req, res, ctx) => res.render('404.njk', ctx)
533
-
534
- // 500 Internal Server Error: Template path or custom handler fn
535
- serverError: 'errors/500.njk', // or (err, req, res, ctx) => ...
536
-
537
- // 503 Request Timeout: Template path or custom handler fn
538
- timeout: 'errors/503.njk' // or (req, res, ctx) => ...
539
- }
60
+ const PORT = process.env.PORT || 3000;
61
+ app.listen(PORT, () => {
62
+ console.log(`Server listening at http://localhost:${PORT}`);
540
63
  });
541
64
  ```
542
65
 
543
- #### Error Template Context & Behavior
544
-
545
- All error templates rendered with Nunjucks automatically receive a rich request context:
546
- - **`404.njk` (`notFound`)**: `{ fsy, locale, isDev, url, method }` — sets HTTP status `404`. If omitted, Webspresso provides a responsive built-in HTML 404 page for browsers, or `{ error: 'Not Found', status: 404 }` JSON for API requests.
547
- - **`500.njk` (`serverError`)**: `{ fsy, locale, isDev, url, method, error, status }` — sets HTTP status `500` (or `err.status`).
548
- - **`503.njk` (`timeout`)**: `{ fsy, locale, isDev, url, method }` — sets HTTP status `503`.
549
-
550
- ```njk
551
- {# Example views/404.njk #}
552
- {% extends "layout.njk" %}
66
+ ### 2. Server-Rendered Page (`pages/index.njk` + `pages/index.js`)
67
+ ```html
68
+ <!-- pages/index.njk -->
69
+ {% extends "views/layout.njk" %}
553
70
 
554
71
  {% block content %}
555
- <div class="max-w-xl mx-auto text-center py-20">
556
- <h1 class="text-6xl font-bold text-slate-800">404</h1>
557
- <p class="text-xl text-slate-600 mt-4">Page Not Found: <code>{{ url }}</code></p>
558
- <a href="{{ fsy.url('/') }}" class="mt-6 inline-block bg-blue-600 text-white px-6 py-2 rounded-lg">
559
- Back to Home
560
- </a>
72
+ <div class="max-w-4xl mx-auto py-12 px-4">
73
+ <h1 class="text-3xl font-bold mb-4">{{ title }}</h1>
74
+ <p class="text-slate-600">Welcome, {{ user.name or "Guest" }}!</p>
561
75
  </div>
562
76
  {% endblock %}
563
77
  ```
564
78
 
565
- **How errors reach this handler**
566
-
567
- Unhandled errors from file-based routes (`pages/**/*.njk` `load()` / middleware / render, and `pages/api/**/*.js` handlers) are forwarded with `next(err)`, so they go through this 4-argument Express error middleware. That means `errorPages.serverError` and `errorPages.timeout` apply to those failures as well (not only to routes you add with `setupRoutes`).
568
-
569
- - **`pages/_hooks.js` `onError(ctx, err)`** runs **before** the central handler (for both SSR and API file routes). Use it for logging or APM (`Sentry.captureException`, `newrelic.noticeError`, etc.). The error is also on **`ctx.error`**.
570
-
571
- - **JSON vs HTML:** Requests whose path starts with **`/api`** always get a **JSON** error body from the default branch (and the default **string** `serverError` / `timeout` Nunjucks templates are skipped for those paths). Other clients use **`Accept`** as before: prefer HTML when the client accepts HTML.
572
-
573
- **Request Timeout:**
574
-
575
- Configure request timeout with `connect-timeout`:
576
-
577
- ```javascript
578
- const { app } = createApp({
579
- pagesDir: './pages',
580
- timeout: '30s', // Default: 30 seconds
581
- // timeout: '1m', // 1 minute
582
- // timeout: false, // Disable timeout
583
- });
584
- ```
585
-
586
- **Asset Management & Cache-Busting:**
587
-
588
- Webspresso includes a built-in `AssetManager` that automates static asset URL resolution, query-string cache-busting, Vite/Webpack build manifest lookups, and CDN prefixing in Nunjucks SSR templates.
589
-
590
- Configure asset handling in `createApp`:
591
-
592
- ```javascript
593
- const { createApp } = require('webspresso');
594
- const path = require('path');
595
-
596
- const { app } = createApp({
597
- pagesDir: './pages',
598
- viewsDir: './views',
599
- publicDir: './public', // default: 'public'
600
- assets: {
601
- // 1. Static query-string versioning (Cache Busting)
602
- // Appends ?v=1.2.3 (or &v=... if existing query params) to asset URLs
603
- version: process.env.APP_VERSION || '1.0.0', // or Git commit hash, build timestamp
604
-
605
- // 2. Build Tool Manifest (Vite, Webpack, etc.)
606
- // Resolves hashed bundle filenames automatically (e.g. style.css -> assets/style-abc123.css)
607
- manifestPath: path.join(__dirname, 'public/.vite/manifest.json'),
608
-
609
- // 3. Asset URL Prefix / CDN
610
- // Prepended to all resolved asset paths
611
- prefix: process.env.CDN_URL || '' // e.g. 'https://cdn.example.com' or '/static'
612
- }
613
- });
614
- ```
615
-
616
- #### How Asset Resolution Works
617
-
618
- 1. **Manifest Match First**: If `manifestPath` is configured and an entry matches the asset key, `AssetManager` resolves to the hashed filename (from Vite or Webpack manifest) and prepends `prefix`.
619
- 2. **Fallback to Path & Versioning**: If no manifest is provided or the asset is not listed in the manifest, `AssetManager` uses the original path, prepends `prefix`, and appends `?v=<version>` (or `&v=<version>`) if `version` is set.
620
- 3. **Leading Slash Normalization**: Paths with or without leading slashes (e.g., `'css/style.css'` or `'/css/style.css'`) are handled seamlessly.
621
-
622
- ```json
623
- // Example Vite manifest format (.vite/manifest.json)
624
- {
625
- "css/style.css": { "file": "assets/style-abc123.css", "src": "css/style.css" },
626
- "js/app.js": { "file": "assets/app-xyz789.js", "src": "js/app.js" }
627
- }
628
-
629
- // Example Webpack manifest format (manifest.json)
630
- {
631
- "/css/style.css": "/dist/style.abc123.css",
632
- "/js/app.js": "/dist/app.xyz789.js"
633
- }
634
- ```
635
-
636
- #### Template Helpers (`fsy`)
637
-
638
- Use `fsy` helpers inside your Nunjucks layouts and pages:
639
-
640
- ```njk
641
- {# 1. URL string resolution #}
642
- <link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
643
- {# Outputs: <link rel="stylesheet" href="/css/style.css?v=1.0.0"> (or /assets/style-abc123.css) #}
644
-
645
- {# 2. CSS link tag helper #}
646
- {{ fsy.css('/css/style.css') | safe }}
647
- {# With custom attributes (media, id, integrity, etc.): #}
648
- {{ fsy.css('/css/print.css', { media: 'print', id: 'print-styles' }) | safe }}
649
-
650
- {# 3. JS script tag helper #}
651
- {{ fsy.js('/js/app.js') | safe }}
652
- {# Supports boolean flags (async, defer) and module type: #}
653
- {{ fsy.js('/js/main.js', { defer: true, type: 'module' }) | safe }}
654
- {# Outputs: <script src="/js/main.js?v=1.0.0" defer type="module"></script> #}
655
-
656
- {# 4. Image tag helper #}
657
- {{ fsy.img('/images/logo.png', 'Company Logo') | safe }}
658
- {# With attributes (loading, class, width, height): #}
659
- {{ fsy.img('/images/hero.jpg', 'Hero Banner', { loading: 'lazy', class: 'w-full rounded', width: 1200, height: 600 }) | safe }}
660
- ```
661
-
662
- #### Programmatic Asset Manager API
663
-
664
- You can also import and use `AssetManager` directly:
665
-
666
- ```javascript
667
- const { AssetManager, configureAssets, getAssetManager } = require('webspresso');
668
-
669
- // Configure global manager
670
- configureAssets({
671
- version: '2.0.0',
672
- prefix: 'https://cdn.example.com'
673
- });
674
-
675
- const manager = getAssetManager();
676
- console.log(manager.asset('/css/style.css')); // -> 'https://cdn.example.com/css/style.css?v=2.0.0'
677
- ```
678
-
679
- **Returns:** `{ app, nunjucksEnv, pluginManager, authMiddleware }` — `authMiddleware` is `null` when `auth` was not passed.
680
-
681
- ### Authentication (session)
682
-
683
- Optional **session-based** auth lives under **`webspresso/core/auth`**: **`createAuth`**, **`quickAuth`**, **`hash`** / **`verify`**, **`setupAuthMiddleware`**, **`createRememberTokensTable`**, and policy helpers. Pass the manager to **`createApp({ auth })`** so routes can use **`middleware: ['auth']`** or **`['guest']`** and handlers can call **`req.auth.attempt()`**, **`req.auth.logout()`**, **`req.auth.can()`**, etc.
684
-
685
- The **admin panel** plugin uses its **own** session and `/api/auth/*` routes (`req.session.adminUser`) — it is separate from **`createApp({ auth })`**.
686
-
687
- For integration patterns (remember-me table, `setupRoutes`, file-router ordering), see **[`doc/index.html#authentication`](doc/index.html#authentication)**.
688
-
689
- ## Plugin System
690
-
691
- Webspresso has a built-in plugin system with version control and dependency management.
692
-
693
- **Error handling:** On plugin errors (missing dependencies, version mismatch, `register()` or `onRoutesReady()` failure), the app does not crash; only a `console.warn` is logged and other plugins continue to run.
694
-
695
- ### Using Plugins
696
-
697
- ```javascript
698
- const { createApp } = require('webspresso');
699
- const { sitemapPlugin, analyticsPlugin, dashboardPlugin } = require('webspresso/plugins');
700
-
701
- const { app } = createApp({
702
- pagesDir: './pages',
703
- viewsDir: './views',
704
- plugins: [
705
- dashboardPlugin(), // Dev dashboard at /_webspresso
706
- sitemapPlugin({
707
- hostname: 'https://example.com',
708
- exclude: ['/admin/*', '/api/*'],
709
- i18n: true,
710
- locales: ['en', 'de']
711
- }),
712
- analyticsPlugin({
713
- google: {
714
- measurementId: 'G-XXXXXXXXXX',
715
- verificationCode: 'xxxxx'
716
- },
717
- yandex: {
718
- counterId: '12345678',
719
- verificationCode: 'xxxxx'
720
- },
721
- bing: {
722
- uetId: '12345678',
723
- verificationCode: 'xxxxx'
724
- }
725
- })
726
- ]
727
- });
728
- ```
729
-
730
- ### Built-in Plugins
731
-
732
- **Dashboard Plugin:**
733
- - Development dashboard at `/_webspresso`
734
- - Monitor all routes (SSR pages and API endpoints)
735
- - View loaded plugins and configuration
736
- - Filter and search routes
737
- - Only active in development mode (disabled in production)
738
-
739
79
  ```javascript
740
- const { dashboardPlugin } = require('webspresso/plugins');
741
-
742
- const { app } = createApp({
743
- pagesDir: './pages',
744
- plugins: [
745
- dashboardPlugin() // Available at /_webspresso in dev mode
746
- ]
747
- });
80
+ // pages/index.js
81
+ module.exports = {
82
+ async load({ req, ctx }) {
83
+ return {
84
+ title: 'Home Page',
85
+ user: req.user || null,
86
+ };
87
+ },
88
+ };
748
89
  ```
749
90
 
750
- Options:
751
- - `path` - Custom dashboard path (default: `/_webspresso`)
752
- - `enabled` - Force enable/disable (default: auto based on NODE_ENV)
753
-
754
- **Redirect plugin:**
755
- - Runs in `register()` **before** file-based routes, so configured paths override SSR pages.
756
- - Rules: `from` (string or `RegExp`), `to` (path or URL), optional `status` (301/302/303/307/308), optional `methods` (`'*'` or a list; default plugin methods are `GET` and `HEAD` only).
757
- - `preserveQuery` (default `true`) appends the request query when `to` has no `?`. External `to` values require `allowExternal: true`.
758
-
91
+ ### 3. Validated JSON API Endpoint (`pages/api/greet.post.js`)
759
92
  ```javascript
760
- const { redirectPlugin } = require('webspresso/plugins');
761
-
762
- redirectPlugin({
763
- rules: [
764
- { from: '/old-blog', to: '/blog', status: 301 },
765
- { from: /^\/wiki\/(.*)$/, to: '/docs' },
766
- ],
767
- });
768
- ```
769
-
770
- **Sitemap Plugin:**
771
- - Generates `/sitemap.xml` from routes automatically
772
- - **Dynamic Database Content**: Generate URLs from database records
773
- - Excludes dynamic routes and API endpoints
774
- - Supports i18n with hreflang tags
775
- - Generates `/robots.txt`
776
- - Configurable caching for performance
93
+ module.exports = {
94
+ schema({ z }) {
95
+ return {
96
+ body: z.object({
97
+ name: z.string().min(2),
98
+ }),
99
+ };
100
+ },
777
101
 
778
- ```javascript
779
- sitemapPlugin({
780
- hostname: 'https://example.com',
781
- db, // Database instance
782
- dynamicSources: [
783
- {
784
- model: 'Post', // Model name
785
- urlPattern: '/blog/:slug', // URL pattern
786
- lastmodField: 'updated_at', // Field for lastmod
787
- filter: (r) => r.published, // Filter records
788
- priority: 0.9,
789
- },
790
- {
791
- // Custom query function
792
- query: async (db) => {
793
- return db.getRepository('Product')
794
- .query()
795
- .where('active', true)
796
- .list();
797
- },
798
- urlPattern: '/products/:slug',
799
- },
800
- ],
801
- })
102
+ async handler(req, res) {
103
+ // req.input is typed and validated by Zod
104
+ const result = await req.service('user.greet', req.input.body);
105
+ res.json(result);
106
+ },
107
+ };
802
108
  ```
803
109
 
804
- **Analytics Plugin:**
805
- - Google Analytics (GA4) and Google Ads
806
- - Google Tag Manager
807
- - Yandex.Metrika
808
- - Microsoft/Bing UET
809
- - Facebook Pixel
810
- - Verification meta tags for all services
110
+ ---
811
111
 
812
- Template helpers from analytics plugin:
112
+ ## 📚 Documentation
813
113
 
814
- ```njk
815
- <head>
816
- {{ fsy.verificationTags() | safe }}
817
- {{ fsy.analyticsHead() | safe }}
818
- </head>
819
- <body>
820
- {{ fsy.analyticsBodyOpen() | safe }}
821
- ...
822
- </body>
823
- ```
114
+ The complete Webspresso documentation is organized into 5 focused pillars:
824
115
 
825
- Individual helpers: `gtag()`, `gtm()`, `gtmNoscript()`, `yandexMetrika()`, `bingUET()`, `facebookPixel()`, `allAnalytics()`
116
+ ### 🚀 Getting Started
117
+ - **[Installation Guide](docs/getting-started/installation.md)** — Node.js 20 LTS prerequisites, CLI setup, and TypeScript.
118
+ - **[Creating Your First App](docs/getting-started/first-app.md)** — Step-by-step walkthrough building an SSR page, API endpoint, and service.
119
+ - **[Project Structure](docs/getting-started/project-structure.md)** — Anatomy of `pages/`, `views/`, `services/`, `models/`, and `config/`.
120
+ - **[Deployment & Production](docs/getting-started/deployment.md)** — Production configuration, Docker, PM2, and Nginx.
826
121
 
827
- **Admin Panel Plugin:**
828
- - Modular admin panel with SPA (Mithril.js)
829
- - Model CRUD UI (auto-generated from ORM)
830
- - Extensible via custom pages, menu items, API routes, and dashboard widgets
831
- - Other plugins (e.g. site-analytics) can register their own admin pages
122
+ ### 📖 Task Guides ("How do I...?")
123
+ - **[File-Based Routing](docs/guides/routing.md)** — Static routes, dynamic params (`[id]`), and catch-all wildcards.
124
+ - **[SSR & Templates](docs/guides/ssr-and-templates.md)** — Nunjucks layouts, `load()` loaders, `fsy` helper catalog, and chunked streaming.
125
+ - **[Services Layer](docs/guides/services.md)** — Defining services, Zod validation, auth role guards, and ACID transactions.
126
+ - **[Database & ORM](docs/guides/database-and-orm.md)** — Model definitions (`defineModel`), `zdb` schema builder, and Knex migrations.
127
+ - **[Dual Authentication](docs/guides/authentication.md)** — Stateful Session cookies for SSR and stateless JWT rotation for REST APIs.
128
+ - **[Admin Panel Customization](docs/guides/admin-panel.md)** — Custom Mithril.js SPA pages, KPI widgets, and custom field renderers.
832
129
 
833
- ```javascript
834
- const { adminPanelPlugin } = require('webspresso/plugins');
130
+ ### 🧠 Concepts & Architecture ("How does it work and why?")
131
+ - **[System Architecture](docs/concepts/architecture.md)** — Layered architecture, subsystem boundaries, and design principles.
132
+ - **[Request Lifecycle](docs/concepts/request-lifecycle.md)** — End-to-end trace from TCP socket to template render and response stream.
133
+ - **[Service Execution Pipeline](docs/concepts/service-execution-pipeline.md)** — Sanitization, Zod validation, auth guards, ambient transactions, and caching.
134
+ - **[Ambient Transaction Propagation](docs/concepts/transaction-propagation.md)** — How `AsyncLocalStorage` manages ACID database transactions across nested services.
835
135
 
836
- const { app } = createApp({
837
- pagesDir: './pages',
838
- plugins: [
839
- adminPanelPlugin({
840
- db,
841
- path: '/_admin', // Admin URL (default: /_admin)
842
- auth: authManager, // Optional: for user management
843
- userManagement: { enabled: true, model: 'User' },
844
- })
845
- ]
846
- });
847
- ```
136
+ ### 📚 API Reference ("What is the exact contract?")
137
+ - **[`createApp()` Configuration](docs/reference/create-app.md)** — Full `CreateAppOptions` parameter reference.
138
+ - **[CLI Reference](docs/reference/cli.md)** — Complete `webspresso` CLI command manual.
139
+ - **[Service Definition Spec](docs/reference/service-definition.md)** — `defineService` options and `ServiceContext` properties.
140
+ - **[Model Definition Spec](docs/reference/model-definition.md)** — `defineModel` options, `zdb` column types, and `Repository` query methods.
141
+ - **[Exceptions Catalog](docs/reference/exceptions.md)** — Built-in framework exception classes and HTTP status codes.
848
142
 
849
- Options:
850
- - `db` (required) - Database instance
851
- - `path` - Admin panel path (default: `/_admin`)
852
- - `auth` - Same **`AuthManager`** instance as **`createApp({ auth })`** when you use **`userManagement`** — enables **Active Sessions** / revoke APIs if **`rememberTokens`** (remember-me) is configured; optional for user CRUD-only
853
- - `userManagement` - Site-user admin UI (`enabled`, `model` matching ORM user table, optional `fields` map). SPA routes: `/_admin/users`, `/_admin/users/new`, …; APIs: `/_admin/api/users*`. Admin staff still use **`admin_users`** / `/_admin` login; this is separate from **`req.user`** on the public site
854
- - `configure` - Callback `(registry) => void` for manual setup
143
+ ---
855
144
 
856
- See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.agents/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
145
+ ## 🤖 For AI Coding Agents
857
146
 
858
- **3-State Table Column Sorting & Configuration:**
147
+ If you are an autonomous coding assistant or LLM working on a Webspresso codebase:
148
+ - Read **[`AGENTS.md`](AGENTS.md)** for authoritative architectural guidelines, zero-regression policies, and critical signatures.
149
+ - Consult **[`llms.txt`](llms.txt)** for a machine-readable sitemap of all documentation resources.
859
150
 
860
- Admin Panel table headers support interactive 3-state column sorting:
861
- - **State 1 (`Unsorted`)**: Default state (`↕` icon).
862
- - **State 2 (`ASC`)**: 1st click sorts ascending (`▲` icon, `?sort=col&order=asc`).
863
- - **State 3 (`DESC`)**: 2nd click sorts descending (`▼` icon, `?sort=col&order=desc`).
864
- - **State 4 (`Unsorted`)**: 3rd click clears sorting and reverts to default primary key ordering.
865
-
866
- Configuration options:
867
- - **Column-level**: Standard scalar columns (`string`, `number`, `boolean`, `timestamp`) are sortable by default. Disable or enable per column:
868
- ```javascript
869
- schema: zdb.schema({
870
- name: zdb.string({ sortable: true }),
871
- secret_notes: zdb.text().config({ sortable: false }), // or zdb.text({ sortable: false })
872
- })
873
- ```
874
- - **Model-level**: Restrict sortable columns or disable sorting completely on a model in `defineModel`:
875
- ```javascript
876
- admin: {
877
- enabled: true,
878
- sortableColumns: ['name', 'price'], // Only name and price are sortable
879
- // Or disable all sorting:
880
- // sortable: false,
881
- }
882
- ```
883
-
884
-
885
-
886
- **Custom Admin Pages (registerModule):**
887
-
888
- Plugins can add custom admin pages using `registerModule` (or `registerPageDir`) in `onRoutesReady`:
889
-
890
- ```javascript
891
- // In your plugin's onRoutesReady(ctx)
892
- const adminApi = ctx.usePlugin('admin-panel');
893
- if (adminApi) {
894
- // Option 1: File-based registration with auto Layout wrapping
895
- adminApi.registerModule({
896
- id: 'my-module',
897
-
898
- pages: [{
899
- id: 'reports',
900
- title: 'Reports',
901
- path: '/reports',
902
- icon: 'chart',
903
- description: 'View reports',
904
- componentFile: path.join(__dirname, 'admin/reports.js'), // JS component file
905
- layout: true, // Auto-wraps view in Admin Layout (default: true)
906
- }],
907
-
908
- menu: [{ id: 'reports', label: 'Reports', path: '/reports', icon: 'chart', order: 5 }],
909
-
910
- api: {
911
- prefix: '/reports',
912
- routes: [
913
- { method: 'get', path: '/summary', handler: getSummaryHandler, auth: true },
914
- ],
915
- },
916
- });
917
-
918
- // Option 2: Folder-based auto registration (dir containing page.json + component.js)
919
- adminApi.registerPageDir(path.join(__dirname, 'admin/pages'));
920
- }
921
- ```
922
-
923
- | Field | Description |
924
- |-------|-------------|
925
- | `id` | Unique module identifier (required) |
926
- | `pages` | Custom admin pages (`id`, `title`, `path`, `icon`, `description`, `url`, `iframeUrl`, `html`, `htmlFile`, `componentFile`, `component`, `layout`) |
927
- | `pagesDir` | Path to directory containing page folders/configs for automatic discovery |
928
- | `menu` | Sidebar menu items (`id`, `label`, `path`, `icon`, `order`) |
929
- | `menuGroups` | Collapsible menu groups (`id`, `label`, `order`) |
930
- | `api` | API routes (`prefix`, `routes`: `method`, `path`, `handler`, `auth`) |
931
- | `widgets` | Dashboard widgets (`id`, `title`, `dataLoader`) |
932
- | `scripts` | External script URLs, script objects, or inline scripts injected into SSR Admin Panel (`registerScript`) |
933
- | `styles` | External stylesheet URLs or inline CSS injected into SSR Admin Panel (`registerStyle`) |
934
-
935
- Pages support JavaScript client components (`componentFile`, `pagesDir`, `component`), external URLs (`url` or `iframeUrl` inside `<iframe>`), as well as **native custom HTML pages** (`html` or `htmlFile` for local HTML files like EUIX Engine apps, Vue, or Alpine). HTML pages render directly in the page DOM with full Tailwind CSS styling, dark mode, and script execution without iframe overhead. Set `iframe: true` to isolate in an iframe if needed. By default (`layout: true`), custom page views are automatically wrapped in the Admin Layout with Breadcrumbs. Set `layout: false` for full-screen / custom canvas pages.
936
-
937
- **Example component file (`admin/reports.js`):**
938
-
939
- ```javascript
940
- // Clean Mithril.js component object (no manual m(Layout, ...) needed!)
941
- ({
942
- oninit: async (vnode) => {
943
- vnode.state.data = [];
944
- try {
945
- const res = await api.get('/reports/summary');
946
- vnode.state.data = res.data || [];
947
- m.redraw();
948
- } catch (err) {
949
- console.error('Failed to load report data:', err);
950
- }
951
- },
952
- view: (vnode) => {
953
- const data = vnode.state.data || [];
954
- return m('div.space-y-4', [
955
- m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', 'Report Summary'),
956
- m('p.text-gray-600.dark:text-slate-400', `Total records: ${data.length}`),
957
- ]);
958
- }
959
- })
960
- ```
961
-
962
-
963
-
964
- **Manual registry API** (alternative to registerModule):
965
-
966
- ```javascript
967
- adminPanelPlugin({
968
- db,
969
- configure(registry) {
970
- registry.registerPage('custom', { title: 'Custom', path: '/custom', icon: 'tool' });
971
- registry.registerClientComponent('custom', 'window.__customPages["custom"] = { view: () => m("p","Hi") };');
972
- registry.registerMenuItem({ id: 'custom', label: 'Custom', path: '/custom', icon: 'tool' });
973
- },
974
- })
975
- ```
976
-
977
- ### Data exchange plugin (`dataExchangePlugin`)
978
-
979
- - **Admin session only** — same `requireAuth` / `req.session.adminUser` as the admin panel; paths live under your admin prefix (default `/_admin`).
980
- - **Excel export** (`.xlsx`) for models with `admin.enabled`; record selection matches the built-in JSON/CSV export (`ids`, `selectAll`, `filters` via query or POST body).
981
- - **Import** — `multipart/form-data` field `file` (`.csv` or `.xlsx`); query/body `mode=insert|upsert`, `upsertKey` (default primary key). Rows are validated through the ORM (`repository.create` / `update`). Hidden columns are excluded; caps: `maxRows`, `maxFileBytes`.
982
- - **UI** — when the plugin is loaded, the admin model list adds **Export Excel**, **Import**, and bulk **Export Excel** (alongside the existing JSON/CSV export actions). Registers bulk action `export-xlsx` for download URLs.
983
- - **Dependencies** — implemented with `exceljs` and `csv-parse` (declared on the `webspresso` package).
984
-
985
- Register **after** `adminPanelPlugin` so session middleware and the admin registry exist; use the **same** `db` and `adminPath`:
986
-
987
- ```javascript
988
- const { adminPanelPlugin, dataExchangePlugin } = require('webspresso/plugins');
989
-
990
- const { app } = createApp({
991
- pagesDir: './pages',
992
- db,
993
- plugins: [
994
- adminPanelPlugin({ db, path: '/_admin' }),
995
- dataExchangePlugin({
996
- db,
997
- adminPath: '/_admin',
998
- maxRows: 10_000,
999
- maxFileBytes: 10 * 1024 * 1024,
1000
- }),
1001
- ],
1002
- });
1003
- ```
1004
-
1005
- Options:
1006
- - `db` — database instance (defaults to `ctx.db` in `onRoutesReady` if omitted but `createApp({ db })` was set)
1007
- - `adminPath` — must match the admin panel path (default `/_admin`)
1008
- - `enabled` — set `false` to skip registering routes (default `true`)
1009
- - `maxRows` — export and import row limit (default `10000`)
1010
- - `maxFileBytes` — multipart upload limit (default 10 MiB)
1011
-
1012
- API (all require admin cookie):
1013
- - `GET|POST ${adminPath}/api/data-exchange/export/:model` — spreadsheet download
1014
- - `POST ${adminPath}/api/data-exchange/import/:model` — import summary `{ success, created, updated, failed, errors: [{ row, message }] }`
1015
-
1016
- **Site Analytics Plugin:**
1017
- - Self-hosted page view analytics (no external services required)
1018
- - Automatic page view tracking via Express middleware
1019
- - Bot detection (40+ patterns: Googlebot, GPTBot, curl, etc.)
1020
- - Country detection (CDN headers, Accept-Language fallback)
1021
- - Admin panel dashboard with Chart.js visualizations
1022
- - Privacy-first: IP addresses are hashed, no cookies required
1023
-
1024
- ```javascript
1025
- const { siteAnalyticsPlugin, adminPanelPlugin } = require('webspresso/plugins');
1026
-
1027
- const { app } = createApp({
1028
- pagesDir: './pages',
1029
- plugins: [
1030
- adminPanelPlugin({ db }),
1031
- siteAnalyticsPlugin({
1032
- db,
1033
- excludePaths: ['/health', '/favicon.ico'],
1034
- trackBots: true, // Record bot visits separately (default: true)
1035
- }),
1036
- ]
1037
- });
1038
- ```
1039
-
1040
- Admin panel analytics page includes:
1041
- - **Summary cards**: Total views, unique visitors, unique pages, sessions
1042
- - **Views over time**: Line chart (Chart.js) with daily views/visitors/sessions
1043
- - **Bot activity**: Bot request counts with horizontal bar visualization
1044
- - **Top pages**: Most viewed pages sorted by view count
1045
- - **Recent activity**: Latest page views with country flags and timestamps
1046
- - **Country stats**: Country breakdown with flag emojis and bar charts
1047
- - **Date filtering**: Last 7, 30, or 90 days toggle
1048
-
1049
- Options:
1050
- - `db` (required) - Database instance
1051
- - `excludePaths` - Additional paths to exclude from tracking (admin, API, and static files are auto-excluded)
1052
- - `trackBots` - Whether to record bot visits (default: `true`)
1053
- - `tableName` - Custom table name (default: `analytics_page_views`)
1054
-
1055
- The `analytics_page_views` table is automatically created on first request.
1056
-
1057
- **Audit log plugin:**
1058
- - Records successful (`2xx`) admin panel model mutations: `create`, `update`, `delete`, `restore` on `${adminPath}/api/models/:model/records…`
1059
- - Actor from `req.session.adminUser` after login; optional IP / user-agent; update metadata stores changed field names only (not full body)
1060
- - `GET ${adminPath}/api/audit-logs` with pagination and filters (`page`, `perPage`, `model`, `action`, `from`, `to`) — use from custom admin pages or the bundled Mithril list (`includeDefaultPage` default `true`)
1061
- - Run `webspresso db:migrate` after adding the migration (see `plugins/audit-log/migration-template.js` or the example under `migrations/`). Prune old rows with the CLI (recommended on a schedule):
1062
-
1063
- ```bash
1064
- npx webspresso audit:prune --days 90
1065
- ```
1066
-
1067
- ```javascript
1068
- const { adminPanelPlugin, auditLogPlugin } = require('webspresso/plugins');
1069
-
1070
- const { app } = createApp({
1071
- pagesDir: './pages',
1072
- plugins: [
1073
- adminPanelPlugin({ db }),
1074
- auditLogPlugin({
1075
- db,
1076
- // adminPath: '/_admin', // must match admin panel `path`
1077
- // tableName: 'audit_logs',
1078
- // includeDefaultPage: true,
1079
- // apiPrefix: '/audit-logs',
1080
- }),
1081
- ],
1082
- });
1083
- ```
1084
-
1085
- Programmatic API (other plugins): `ctx.usePlugin('audit-log')` exposes `queryLogs`, `purgeAuditLogs`, and `getMigrationTemplate()`.
1086
-
1087
- **Email plugin:**
1088
- - Sends email via **Nodemailer** with **MJML** templates (`{{variable}}` interpolation)
1089
- - Template sources: `templatesDir` (`*.mjml`, **Node.js only** — skipped on edge/workers), `templates` map with inline `{ mjml }`, runtime `registerTemplate`, or inline `send({ mjml, html, data })`
1090
- - Transport: `transport`, `smtp`, or env (`SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_SECURE`, `MAIL_FROM`)
1091
- - Optional DB logging to `email_logs` when `db` is passed; prune with `webspresso email:prune --days 90`
1092
- - **Build:** `webspresso build` embeds `emails/*.mjml` (config `emailDir`) + bundled auth templates into `manifest.emailTemplates` and `email-templates.mjs` (Cloudflare). Use `emailPlugin({ manifest })` or `emailPlugin({ emailTemplates: manifest.emailTemplates })` on edge.
1093
- - Admin page at `/_admin/email` (test send, preview, SMTP verify, logs) when `adminPanelPlugin` is loaded
1094
- - Optional **auth bridge**: `authEmails: { enabled: true }` sends password-reset / email-verification mails and can register `POST /api/auth/forgot-password`, `/reset-password`, `/verify-email`, `/resend-verification`
1095
-
1096
- ```javascript
1097
- const { quickAuth, createAuthTokensTable } = require('webspresso/core/auth');
1098
- const { adminPanelPlugin, emailPlugin } = require('webspresso/plugins');
1099
-
1100
- const auth = quickAuth({ db, authTokens: true });
1101
- await createAuthTokensTable(db.knex);
1102
-
1103
- const { app, pluginManager } = createApp({
1104
- pagesDir: './pages',
1105
- db,
1106
- auth,
1107
- plugins: [
1108
- emailPlugin({
1109
- db,
1110
- auth,
1111
- templatesDir: './emails',
1112
- smtp: {
1113
- host: process.env.SMTP_HOST,
1114
- port: 587,
1115
- auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
1116
- },
1117
- defaults: { from: process.env.MAIL_FROM },
1118
- authEmails: {
1119
- enabled: true,
1120
- baseUrl: process.env.BASE_URL,
1121
- registerRoutes: true,
1122
- },
1123
- }),
1124
- adminPanelPlugin({ db }),
1125
- ],
1126
- });
1127
-
1128
- // From a route handler or another plugin:
1129
- await pluginManager.getPluginAPI('email').sendTemplate('welcome', {
1130
- to: 'user@example.com',
1131
- subject: 'Welcome',
1132
- data: { name: 'Ali' },
1133
- });
1134
- ```
1135
-
1136
- Programmatic API: `ctx.usePlugin('email')` exposes `send`, `sendTemplate`, `preview`, `registerTemplate`, `listTemplates`, `verifyConnection`, `queryLogs`, `purgeLogs`, `getMigrationTemplate()`.
1137
-
1138
- **ORM cache admin plugin:**
1139
- - Depends on **`adminPanelPlugin`** and a database instance created with **`createDatabase({ cache: true | { … } })`** so **`db.cache`** is non-null
1140
- - Registers an **ORM Cache** admin page (metrics, full purge, per-model invalidation, reset counters) backed by **`db.cache`**
1141
- - If `db.cache` is disabled, the plugin logs a warning and skips registration
1142
-
1143
- ```javascript
1144
- const { adminPanelPlugin, ormCacheAdminPlugin } = require('webspresso');
1145
-
1146
- const { app } = createApp({
1147
- pagesDir: './pages',
1148
- plugins: [
1149
- adminPanelPlugin({ db }),
1150
- ormCacheAdminPlugin({ db }),
1151
- ],
1152
- });
1153
- ```
1154
-
1155
- **reCAPTCHA plugin:**
1156
- - Google reCAPTCHA **v2** (checkbox) or **v3** (score): server verification via `https://www.google.com/recaptcha/api/siteverify` (no extra npm dependency; Node 18+ `fetch`)
1157
- - Registers CSP entries for Google scripts/iframes; Nunjucks helpers: `recaptchaScript`, `recaptchaWidget` (v2), `recaptchaV3Token` (hidden input + execute for v3 — use with `version: 'v3'` and `recaptchaScript` loads `api.js?render=siteKey`)
1158
- - Secret key: `options.secretKey` or env `RECAPTCHA_SECRET_KEY` (never expose to templates)
1159
-
1160
- ```javascript
1161
- const {
1162
- recaptchaPlugin,
1163
- createRecaptchaMiddleware,
1164
- resolveRecaptchaMiddlewareParams,
1165
- } = require('webspresso/plugins/recaptcha');
1166
-
1167
- const recaptchaConfig = {
1168
- siteKey: process.env.RECAPTCHA_SITE_KEY,
1169
- secretKey: process.env.RECAPTCHA_SECRET_KEY,
1170
- version: 'v2', // or 'v3'
1171
- minScore: 0.5,
1172
- expectedAction: 'contact', // v3 verification only
1173
- defaultV3Action: 'submit', // for recaptchaV3Token helper
1174
- };
1175
-
1176
- const { app } = createApp({
1177
- pagesDir: './pages',
1178
- plugins: [recaptchaPlugin(recaptchaConfig)],
1179
- middlewares: {
1180
- recaptcha: createRecaptchaMiddleware({
1181
- ...resolveRecaptchaMiddlewareParams(recaptchaConfig),
1182
- bodyField: 'g-recaptcha-response',
1183
- }),
1184
- },
1185
- });
1186
- ```
1187
-
1188
- **File-based API** (`pages/api/...post.js`): use the named middleware instead of duplicating verification in the handler; on success, `req.recaptcha` contains a short summary of Google’s response.
1189
-
1190
- ```javascript
1191
- // pages/api/contact.post.js
1192
- module.exports = async function post(req, res) {
1193
- // recaptcha middleware already returned 400 if token invalid
1194
- return res.json({ ok: true, hostname: req.recaptcha.hostname });
1195
- };
1196
-
1197
- module.exports.middleware = ['recaptcha'];
1198
- ```
1199
-
1200
- Optional: after `createApp`, use `pluginManager.getPluginAPI('recaptcha').createMiddleware({ bodyField: '...' })` to override plugin defaults and attach to the `app.use` chain (after body parsers).
1201
-
1202
- Low-level usage (without middleware): import `verifyRecaptcha` / `getRemoteIp` from `webspresso/plugins/recaptcha/verify`.
1203
-
1204
- **SEO Checker Plugin:**
1205
- - Client-side SEO analysis tool (inspired by django-check-seo)
1206
- - Integrated with dev toolbar
1207
- - 40+ SEO checks across 7 categories
1208
- - Real-time analysis with score calculation
1209
- - Only active in development mode
1210
-
1211
- ```javascript
1212
- const { seoCheckerPlugin } = require('webspresso/plugins');
1213
-
1214
- const { app } = createApp({
1215
- pagesDir: './pages',
1216
- plugins: [
1217
- seoCheckerPlugin({
1218
- settings: {
1219
- titleLength: [30, 60], // Min/max title length
1220
- descriptionLength: [50, 160], // Min/max description length
1221
- minContentWords: 300, // Minimum content words
1222
- minInternalLinks: 1, // Minimum internal links
1223
- minExternalLinks: 1, // Minimum external links
1224
- maxUrlLength: 75, // Maximum URL length
1225
- maxUrlDepth: 3 // Maximum URL depth
1226
- }
1227
- })
1228
- ]
1229
- });
1230
- ```
1231
-
1232
- SEO Check Categories:
1233
- | Category | Checks |
1234
- |----------|--------|
1235
- | **Meta** | Title, Description, Canonical, Viewport, Robots, Charset, Lang |
1236
- | **Headings** | H1 existence, Single H1, Hierarchy, Non-empty headings |
1237
- | **Content** | Word count, Paragraphs, Keyword usage, Keywords early |
1238
- | **Links** | Internal links, External links, Nofollow, Anchor text |
1239
- | **Images** | Alt text, Descriptive alt, Dimensions, Lazy loading |
1240
- | **Structured** | Open Graph, Twitter Card, JSON-LD, Hreflang |
1241
- | **URL** | Length, Depth, Readability, HTTPS |
1242
-
1243
- The SEO Checker panel appears as a floating widget and can be opened via:
1244
- - Dev toolbar "SEO Check" button
1245
- - Floating toggle button (🔍) in bottom-right corner
1246
-
1247
- ### Creating Custom Plugins
1248
-
1249
- ```javascript
1250
- const myPlugin = {
1251
- name: 'my-plugin',
1252
- version: '1.0.0',
1253
-
1254
- // Optional: depend on other plugins
1255
- dependencies: {
1256
- 'analytics': '^1.0.0'
1257
- },
1258
-
1259
- // Optional: expose API for other plugins
1260
- api: {
1261
- getData() { return this.data; }
1262
- },
1263
-
1264
- // Called during registration
1265
- register(ctx) {
1266
- // Access Express app
1267
- ctx.app.use((req, res, next) => next());
1268
-
1269
- // Access database (when createApp({ db }) is used)
1270
- if (ctx.db) {
1271
- // Use ctx.db.getRepository('Model'), ctx.db.knex, etc.
1272
- }
1273
-
1274
- // Add template helpers
1275
- ctx.addHelper('myHelper', () => 'Hello!');
1276
-
1277
- // Add Nunjucks filters
1278
- ctx.addFilter('myFilter', (val) => val.toUpperCase());
1279
-
1280
- // Use other plugins
1281
- const analytics = ctx.usePlugin('analytics');
1282
- },
1283
-
1284
- // Called after all routes are mounted
1285
- onRoutesReady(ctx) {
1286
- // ctx.db available when createApp({ db }) is used
1287
- // Access route metadata
1288
- console.log('Routes:', ctx.routes);
1289
-
1290
- // Add custom routes
1291
- ctx.addRoute('get', '/my-route', (req, res) => {
1292
- res.json({ hello: 'world' });
1293
- });
1294
- },
1295
-
1296
- // Called before server starts
1297
- onReady(ctx) {
1298
- console.log('Server ready!');
1299
- }
1300
- };
1301
-
1302
- // Use as factory function for configuration
1303
- function myPluginFactory(options = {}) {
1304
- return {
1305
- name: 'my-plugin',
1306
- version: '1.0.0',
1307
- _options: options,
1308
- register(ctx) {
1309
- // ctx.options contains the passed options
1310
- }
1311
- };
1312
- }
1313
- ```
1314
-
1315
- ### Plugin Error Handling
1316
-
1317
- The plugin system does not crash the app on errors; it only logs warnings:
1318
-
1319
- - **Missing dependency**: If a plugin in `dependencies` is not loaded → warning, plugin still loads
1320
- - **Version mismatch**: If a dependent plugin version is incompatible → warning, plugin still loads
1321
- - **Circular dependency**: If two plugins depend on each other → warning, plugins in the cycle are skipped
1322
- - **Duplicate plugin name**: Two plugins with the same name → warning, second one is skipped
1323
- - **`register()` error**: If `register(ctx)` throws → warning, plugin is removed from the registry
1324
- - **`onRoutesReady()` error**: If the hook throws → warning, server stays up
1325
-
1326
- A single faulty plugin does not block the entire application.
1327
-
1328
- ## File-Based Routing
1329
-
1330
- ### SSR Pages
1331
-
1332
- Create `.njk` files in the `pages/` directory:
1333
-
1334
- | File Path | Route |
1335
- |-----------|-------|
1336
- | `pages/index.njk` | `/` |
1337
- | `pages/about/index.njk` | `/about` |
1338
- | `pages/tools/[slug].njk` | `/tools/:slug` |
1339
- | `pages/docs/[...rest].njk` | `/docs/*` |
1340
-
1341
- ### API Routes
1342
-
1343
- Create `.js` files in `pages/api/` with optional method suffixes:
1344
-
1345
- | File Path | Route |
1346
- |-----------|-------|
1347
- | `pages/api/health.get.js` | `GET /api/health` |
1348
- | `pages/api/echo.post.js` | `POST /api/echo` |
1349
- | `pages/api/users/[id].get.js` | `GET /api/users/:id` |
1350
-
1351
- **Basic API Handler:**
1352
-
1353
- ```javascript
1354
- // pages/api/health.get.js
1355
- module.exports = async function handler(req, res) {
1356
- res.json({ status: 'ok' });
1357
- };
1358
- ```
1359
-
1360
- **Object export (`handler`, `middleware`, `schema`):**
1361
-
1362
- Use a single export object when you need **named middleware** from `createApp({ middlewares })` and/or a **Zod** `schema`. Order at runtime: **`req.db`** (if configured) → **schema** (`req.input`) → **middleware** chain → **handler**.
1363
-
1364
- ```javascript
1365
- // pages/api/search.post.js
1366
- module.exports = {
1367
- middleware: ['requireAuth'],
1368
- schema: ({ z }) => ({
1369
- body: z.object({ q: z.string() }),
1370
- }),
1371
- handler: async (req, res) => {
1372
- const { q } = req.input.body;
1373
- const results = [];
1374
- return res.json({ results, q });
1375
- },
1376
- };
1377
- ```
1378
-
1379
- Register `requireAuth` (and any other names) on the app:
1380
-
1381
- ```javascript
1382
- createApp({
1383
- pagesDir: './pages',
1384
- middlewares: {
1385
- requireAuth: (req, res, next) => {
1386
- if (!req.session?.user) return res.status(401).json({ error: 'Unauthorized' });
1387
- next();
1388
- },
1389
- },
1390
- });
1391
- ```
1392
-
1393
- ### Authentication & JWT Support (`quickAuth`)
1394
-
1395
- Webspresso provides a zero-dependency **Dual Authentication** system (`webspresso/core/auth`):
1396
- - **Session + Cookie**: Stateful auth for browser SSR routes and Admin Panel (`/`, `/_admin`).
1397
- - **JWT + Refresh Token**: Stateless HS256 auth for REST APIs, Mobile Apps, and SPAs (`/api/*`).
1398
-
1399
- ```javascript
1400
- // createApp setup with quickAuth
1401
- const { quickAuth } = require('webspresso/core/auth');
1402
-
1403
- const auth = quickAuth({
1404
- db,
1405
- userModel: 'User',
1406
- jwt: {
1407
- expiresIn: '15m', // Access token duration (default: 24h)
1408
- refreshExpiresIn: '30d', // Refresh token duration (default: 30d)
1409
- },
1410
- });
1411
-
1412
- createApp({
1413
- pagesDir: './pages',
1414
- db,
1415
- auth,
1416
- });
1417
- ```
1418
-
1419
- **File-Based API Route Examples (`pages/api/*`):**
1420
-
1421
- ```javascript
1422
- // 1. pages/api/auth/login.post.js (Login & Token Generation)
1423
- module.exports = {
1424
- schema: ({ z }) => ({
1425
- body: z.object({
1426
- email: z.string().email(),
1427
- password: z.string().min(1),
1428
- }),
1429
- }),
1430
- async handler(req, res) {
1431
- const { email, password } = req.input.body;
1432
- const user = await req.auth.attempt(email, password);
1433
- if (!user) return res.status(401).json({ error: 'Invalid credentials' });
1434
-
1435
- const accessToken = req.auth.generateUserToken(user);
1436
- const refreshToken = req.auth.generateRefreshToken(user);
1437
-
1438
- return res.json({ accessToken, refreshToken, user: { id: user.id, email: user.email } });
1439
- },
1440
- };
1441
- ```
1442
-
1443
- ```javascript
1444
- // 2. pages/api/auth/refresh.post.js (Token Rotation Endpoint)
1445
- module.exports = {
1446
- schema: ({ z }) => ({
1447
- body: z.object({
1448
- refreshToken: z.string(),
1449
- }),
1450
- }),
1451
- async handler(req, res) {
1452
- const { refreshToken } = req.input.body;
1453
- try {
1454
- const result = await req.auth.refreshAccessToken(refreshToken, { rotate: true });
1455
- return res.json({ accessToken: result.accessToken, refreshToken: result.refreshToken });
1456
- } catch (err) {
1457
- return res.status(401).json({ error: err.message });
1458
- }
1459
- },
1460
- };
1461
- ```
1462
-
1463
- ```javascript
1464
- // 3. pages/api/profile.get.js (Protected REST API Route)
1465
- module.exports = {
1466
- middleware: ['jwt'],
1467
- async handler(req, res) {
1468
- return res.json({ user: req.user, token: req.token });
1469
- },
1470
- };
1471
- ```
1472
-
1473
- **With Schema Validation (same object shape):**
1474
-
1475
- ```javascript
1476
- // pages/api/posts.post.js
1477
- module.exports = {
1478
- schema: ({ z }) => ({
1479
- body: z.object({
1480
- title: z.string().min(3).max(100),
1481
- content: z.string(),
1482
- tags: z.array(z.string()).optional()
1483
- }),
1484
- query: z.object({
1485
- draft: z.coerce.boolean().default(false)
1486
- })
1487
- }),
1488
-
1489
- async handler(req, res) {
1490
- // Validated & parsed data available in req.input
1491
- const { title, content, tags } = req.input.body;
1492
- const { draft } = req.input.query;
1493
-
1494
- // Original req.body, req.query remain untouched
1495
- res.json({ success: true, title, draft });
1496
- }
1497
- };
1498
- ```
1499
-
1500
- **Schema Options:**
1501
-
1502
- | Key | Description |
1503
- |-----|-------------|
1504
- | `body` | Validates `req.body` (POST/PUT/PATCH) |
1505
- | `params` | Validates route parameters (e.g., `:id`) |
1506
- | `query` | Validates query string parameters |
1507
- | `response` | Response schema (for documentation, not enforced) |
1508
-
1509
- All schemas use [Zod](https://zod.dev) for validation. Invalid requests receive **`400 JSON`**: `{ error: 'Validation Error', issues: [...] }` (no `handler` / `middleware` run).
1510
-
1511
- For **nanoid** route parameters (same alphabet and default length as **`generateNanoid`** / **`zdb.nanoid()`**), use **`z.nanoid()`** on the `z` passed into your API schema (Webspresso extends it; no extra import). Optional: **`z.nanoid(12)`** or **`z.nanoid({ maxLength: 12 })`** (matches **`zdb.nanoid({ maxLength })`**). For scripts or tests outside `schema: ({ z })`, **`zodNanoid(z, size?)`** and **`extendZ(z)`** are exported from `webspresso`.
1512
-
1513
- ```javascript
1514
- module.exports = {
1515
- schema: ({ z }) => ({
1516
- params: z.object({
1517
- id: z.nanoid(), // default 21 chars
1518
- shortId: z.nanoid(12), // numeric length
1519
- other: z.nanoid({ maxLength: 8 }), // zdb-style options
1520
- }),
1521
- }),
1522
- async handler(req, res) {
1523
- const { id } = req.input.params;
1524
- // ...
1525
- },
1526
- };
1527
- ```
1528
-
1529
- **Dynamic Route with Params Validation:**
1530
-
1531
- ```javascript
1532
- // pages/api/users/[id].get.js
1533
- module.exports = {
1534
- schema: ({ z }) => ({
1535
- params: z.object({
1536
- id: z.string().uuid()
1537
- }),
1538
- query: z.object({
1539
- fields: z.string().optional()
1540
- })
1541
- }),
1542
-
1543
- async handler(req, res) {
1544
- const { id } = req.input.params; // Validated UUID
1545
- const user = await getUser(id);
1546
- res.json(user);
1547
- }
1548
- };
1549
- ```
1550
-
1551
- ### Route Config
1552
-
1553
- Add a `.js` file alongside your `.njk` file to configure the route:
1554
-
1555
- ```javascript
1556
- // pages/tools/index.js
1557
- module.exports = {
1558
- // Middleware for this route
1559
- middleware: [(req, res, next) => next()],
1560
-
1561
- // Load data for SSR
1562
- async load(req, ctx) {
1563
- // ctx.db is available when createApp({ db }) is used
1564
- if (ctx.db) {
1565
- const posts = await ctx.db.getRepository('Post').query().limit(10);
1566
- return { posts };
1567
- }
1568
- return { tools: await fetchTools() };
1569
- },
1570
-
1571
- // Override meta tags
1572
- meta(req, ctx) {
1573
- return {
1574
- title: 'Tools',
1575
- description: 'Developer tools'
1576
- };
1577
- },
1578
-
1579
- // Route-level hooks
1580
- hooks: {
1581
- beforeLoad: async (ctx) => {},
1582
- afterRender: async (ctx) => {}
1583
- }
1584
- };
1585
- ```
1586
-
1587
- ## i18n
1588
-
1589
- ### Global Translations
1590
-
1591
- Add JSON files to `pages/locales/`:
1592
-
1593
- ```json
1594
- {
1595
- "nav": {
1596
- "home": "Home",
1597
- "about": "About"
1598
- }
1599
- }
1600
- ```
1601
-
1602
- ### Route-Specific Translations
1603
-
1604
- Add a `locales/` folder inside any route directory to override global translations.
1605
-
1606
- ### Using Translations
1607
-
1608
- In templates:
1609
-
1610
- ```nunjucks
1611
- <h1>{{ t('nav.home') }}</h1>
1612
- ```
1613
-
1614
- ## Template Helpers
1615
-
1616
- The `fsy` object is available in all templates:
1617
-
1618
- ```nunjucks
1619
- {# URL helpers #}
1620
- {{ fsy.url('/tools', { page: 1 }) }}
1621
- {{ fsy.fullUrl('/tools') }}
1622
- {{ fsy.route('/tools/:slug', { slug: 'test' }) }}
1623
-
1624
- {# Request helpers #}
1625
- {{ fsy.q('page', 1) }}
1626
- {{ fsy.param('slug') }}
1627
- {{ fsy.hdr('User-Agent') }}
1628
-
1629
- {# Utility helpers #}
1630
- {{ fsy.slugify('Hello World') }}
1631
- {{ fsy.truncate(text, 100) }}
1632
- {{ fsy.prettyBytes(1024) }}
1633
- {{ fsy.prettyMs(5000) }}
1634
-
1635
- {# Date/Time helpers (dayjs) #}
1636
- {{ fsy.dateFormat(post.created_at, 'YYYY-MM-DD HH:mm') }}
1637
- {{ fsy.dateFromNow(post.created_at) }} {# "2 hours ago" #}
1638
- {{ fsy.dateAgo(post.created_at) }} {# "2 hours ago" #}
1639
- {{ fsy.dateUntil(event.date) }} {# "in 2 hours" #}
1640
- {{ fsy.date(post.created_at).format('MMMM D, YYYY') }} {# Full dayjs API #}
1641
- {% if fsy.dateIsBefore(post.created_at, fsy.date()) %}Published{% endif %}
1642
- {{ fsy.dateDiff(post.created_at, fsy.date(), 'day') }} days ago
1643
- {{ fsy.dateAdd(post.created_at, 7, 'day').format('YYYY-MM-DD') }}
1644
- {{ fsy.dateStartOf(post.created_at, 'month').format('YYYY-MM-DD') }}
1645
-
1646
- {# Environment #}
1647
- {% if fsy.isDev() %}Dev mode{% endif %}
1648
-
1649
- {# SEO #}
1650
- {{ fsy.canonical() }}
1651
- {{ fsy.jsonld(schema) | safe }}
1652
- ```
1653
-
1654
- ## Lifecycle Hooks
1655
-
1656
- ### Global Hooks
1657
-
1658
- Create `pages/_hooks.js`:
1659
-
1660
- ```javascript
1661
- module.exports = {
1662
- onRequest(ctx) {},
1663
- beforeLoad(ctx) {},
1664
- afterLoad(ctx) {},
1665
- beforeRender(ctx) {},
1666
- afterRender(ctx) {},
1667
- onError(ctx, err) {}
1668
- };
1669
- ```
1670
-
1671
- ### Hook Execution Order
1672
-
1673
- 1. Global `onRequest`
1674
- 2. Route `onRequest`
1675
- 3. Route `beforeMiddleware`
1676
- 4. Route middleware
1677
- 5. Route `afterMiddleware`
1678
- 6. Route `beforeLoad`
1679
- 7. Route `load()`
1680
- 8. Route `afterLoad`
1681
- 9. Route `beforeRender`
1682
- 10. Nunjucks render
1683
- 11. Route `afterRender`
1684
-
1685
- ## Environment Variables
1686
-
1687
- | Variable | Default | Description |
1688
- |----------|---------|-------------|
1689
- | `NODE_ENV` | `development` | Environment |
1690
- | `DEFAULT_LOCALE` | `en` | Default locale |
1691
- | `SUPPORTED_LOCALES` | `en` | Comma-separated locales |
1692
- | `BASE_URL` | `http://localhost:3000` | Base URL for canonical URLs |
1693
- | `DATABASE_URL` | - | Database connection string (for ORM) |
1694
-
1695
- ## ORM (Database)
1696
-
1697
- Webspresso includes a minimal, Eloquent-inspired ORM built on Knex with Zod schemas as the single source of truth.
1698
-
1699
- ### Quick Start
1700
-
1701
- ```javascript
1702
- const { zdb, defineModel, createDatabase } = require('webspresso');
1703
-
1704
- // 1. Define your schema with database metadata
1705
- const UserSchema = zdb.schema({
1706
- id: zdb.id(),
1707
- email: zdb.string({ unique: true, index: true }),
1708
- name: zdb.string({ maxLength: 100 }),
1709
- status: zdb.enum(['active', 'inactive'], { default: 'active' }),
1710
- company_id: zdb.foreignKey('companies', { nullable: true }),
1711
- created_at: zdb.timestamp({ auto: 'create' }),
1712
- updated_at: zdb.timestamp({ auto: 'update' }),
1713
- deleted_at: zdb.timestamp({ nullable: true }),
1714
- });
1715
-
1716
- // 3. Define your model
1717
- const User = defineModel({
1718
- name: 'User',
1719
- table: 'users',
1720
- schema: UserSchema,
1721
- relations: {
1722
- company: { type: 'belongsTo', model: () => Company, foreignKey: 'company_id' },
1723
- posts: { type: 'hasMany', model: () => Post, foreignKey: 'user_id' },
1724
- },
1725
- scopes: { softDelete: true, timestamps: true },
1726
- });
1727
-
1728
- // 4. Create database (models auto-loaded from ./models directory)
1729
- const db = createDatabase({
1730
- client: 'pg',
1731
- connection: process.env.DATABASE_URL,
1732
- models: './models', // Optional, defaults to './models'
1733
- });
1734
-
1735
- // Models are automatically loaded from models/ directory
1736
- // Use getRepository with model name
1737
- const UserRepo = db.getRepository('User');
1738
- const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
1739
- ```
1740
-
1741
- ### Schema Helpers (zdb)
1742
-
1743
- The `zdb` helpers wrap Zod schemas with database column metadata:
1744
-
1745
- | Helper | Description | Options |
1746
- |--------|-------------|---------|
1747
- | `zdb.id()` | Primary key (bigint, auto-increment) | |
1748
- | `zdb.uuid()` | UUID primary key | |
1749
- | `zdb.nanoid(opts)` | Nanoid primary key (URL-safe string, stored as VARCHAR) | `maxLength` (default `21`) |
1750
- | `zdb.string(opts)` | VARCHAR column | `maxLength`, `unique`, `index`, `nullable` |
1751
- | `zdb.text(opts)` | TEXT column | `nullable` |
1752
- | `zdb.integer(opts)` | INTEGER column | `nullable`, `default` |
1753
- | `zdb.bigint(opts)` | BIGINT column | `nullable` |
1754
- | `zdb.float(opts)` | FLOAT column | `nullable` |
1755
- | `zdb.decimal(opts)` | DECIMAL column | `precision`, `scale`, `nullable` |
1756
- | `zdb.boolean(opts)` | BOOLEAN column | `default`, `nullable` |
1757
- | `zdb.date(opts)` | DATE column | `nullable` |
1758
- | `zdb.datetime(opts)` | DATETIME column | `nullable` |
1759
- | `zdb.timestamp(opts)` | TIMESTAMP column | `auto: 'create'\|'update'`, `nullable` |
1760
- | `zdb.json(opts)` | JSON column | `nullable` |
1761
- | `zdb.array(itemSchema, opts)` | ARRAY column (stored as JSON) | `nullable` |
1762
- | `zdb.enum(values, opts)` | ENUM column | `default`, `nullable` |
1763
- | `zdb.foreignKey(table, opts)` | Foreign key (bigint) | `referenceColumn`, `nullable` |
1764
- | `zdb.foreignUuid(table, opts)` | Foreign key (uuid) | `referenceColumn`, `nullable` |
1765
- | `zdb.foreignNanoid(table, opts)` | Foreign key (nanoid string) | `referenceColumn`, `nullable`, `maxLength` (must match referenced PK) |
1766
-
1767
- **Nanoid columns:** Migration scaffolding emits `table.string(column, maxLength)`. For a **nanoid primary key**, if you omit the primary key on `repository.create()`, Webspresso fills it with a cryptographically random ID using the same default alphabet as the [`nanoid`](https://github.com/ai/nanoid) package (implemented in-framework; no extra dependency). You can also call **`generateNanoid`** (exported from `webspresso`) anywhere you need the same generator.
1768
-
1769
- ### Model Definition
1770
-
1771
- ```javascript
1772
- const User = defineModel({
1773
- name: 'User', // Model name
1774
- table: 'users', // Database table
1775
- schema: UserSchema, // Zod schema
1776
- primaryKey: 'id', // Primary key column (default: 'id')
1777
-
1778
- relations: {
1779
- // belongsTo: this model has foreign key
1780
- company: {
1781
- type: 'belongsTo',
1782
- model: () => Company,
1783
- foreignKey: 'company_id',
1784
- },
1785
- // hasMany: related model has foreign key
1786
- posts: {
1787
- type: 'hasMany',
1788
- model: () => Post,
1789
- foreignKey: 'user_id',
1790
- },
1791
- // hasOne: like hasMany but returns single record
1792
- profile: {
1793
- type: 'hasOne',
1794
- model: () => Profile,
1795
- foreignKey: 'user_id',
1796
- },
1797
- },
1798
-
1799
- scopes: {
1800
- softDelete: true, // Use deleted_at column
1801
- timestamps: true, // Auto-manage created_at/updated_at
1802
- tenant: 'tenant_id', // Multi-tenant column (optional)
1803
- },
1804
-
1805
- hidden: ['password_hash', 'api_token'], // Never expose in API/templates (security)
1806
- });
1807
- ```
1808
-
1809
- **Hidden columns:** Add column names to `hidden` so they are never exposed in admin API responses, exports, or when passing to templates. Use for sensitive data like `password_hash`, `api_token`, `secret_key`. The admin panel will exclude these from list views and forms automatically.
1810
-
1811
- ### Auto-Loading Models
1812
-
1813
- Models are automatically loaded from the `models/` directory when you create a database instance:
1814
-
1815
- ```javascript
1816
- // models/User.js
1817
- const { defineModel, zdb } = require('webspresso');
1818
-
1819
- module.exports = defineModel({
1820
- name: 'User',
1821
- table: 'users',
1822
- schema: zdb.schema({
1823
- id: zdb.id(),
1824
- email: zdb.string({ unique: true }),
1825
- name: zdb.string(),
1826
- created_at: zdb.timestamp({ auto: 'create' }),
1827
- updated_at: zdb.timestamp({ auto: 'update' }),
1828
- }),
1829
- });
1830
-
1831
- // In your application code
1832
- const db = createDatabase({
1833
- client: 'pg',
1834
- connection: process.env.DATABASE_URL,
1835
- models: './models', // Optional, defaults to './models'
1836
- });
1837
-
1838
- // Models are automatically loaded, use getRepository with model name
1839
- const UserRepo = db.getRepository('User');
1840
- ```
1841
-
1842
- Pass `db` to `createApp({ db })` to expose it as `ctx.db` in plugin hooks and page `load`/`meta` functions.
1843
-
1844
- **Model File Structure:**
1845
- - Place model files in `models/` directory (or custom path via `config.models`)
1846
- - Each file should export a model defined with `defineModel()`
1847
- - Files starting with `_` are ignored (useful for shared utilities)
1848
- - Models are loaded in alphabetical order
1849
-
1850
- ### Repository API
1851
-
1852
- ```javascript
1853
- const db = createDatabase({ client: 'pg', connection: '...' });
1854
- const UserRepo = db.getRepository('User'); // Use model name string
1855
-
1856
- // Find by ID (with eager loading)
1857
- const user = await UserRepo.findById(1, { with: ['company', 'posts'] });
1858
-
1859
- // Find one by conditions
1860
- const admin = await UserRepo.findOne({ email: 'admin@example.com' });
1861
-
1862
- // Find all
1863
- const users = await UserRepo.findAll({ with: ['company'] });
1864
-
1865
- // Create
1866
- const newUser = await UserRepo.create({
1867
- email: 'new@example.com',
1868
- name: 'New User',
1869
- });
1870
-
1871
- // Create many
1872
- const users = await UserRepo.createMany([
1873
- { email: 'user1@test.com', name: 'User 1' },
1874
- { email: 'user2@test.com', name: 'User 2' },
1875
- ]);
1876
-
1877
- // Update
1878
- const updated = await UserRepo.update(1, { name: 'Updated Name' });
1879
-
1880
- // Update where
1881
- await UserRepo.updateWhere({ status: 'inactive' }, { status: 'banned' });
1882
-
1883
- // Delete (soft delete if enabled)
1884
- await UserRepo.delete(1);
1885
-
1886
- // Force delete (permanent)
1887
- await UserRepo.forceDelete(1);
1888
-
1889
- // Restore soft-deleted
1890
- await UserRepo.restore(1);
1891
-
1892
- // Count
1893
- const count = await UserRepo.count({ status: 'active' });
1894
-
1895
- // Exists
1896
- const exists = await UserRepo.exists({ email: 'test@example.com' });
1897
- ```
1898
-
1899
- ### Query Builder
1900
-
1901
- ```javascript
1902
- const users = await UserRepo.query()
1903
- .where({ status: 'active' })
1904
- .where('created_at', '>', '2024-01-01')
1905
- .whereIn('role', ['admin', 'moderator'])
1906
- .whereNotNull('email_verified_at')
1907
- .orderBy('name', 'asc')
1908
- .orderBy('created_at', 'desc')
1909
- .limit(10)
1910
- .offset(20)
1911
- .with('company', 'posts')
1912
- .list();
1913
-
1914
- // First result
1915
- const user = await UserRepo.query()
1916
- .where({ email: 'admin@example.com' })
1917
- .first();
1918
-
1919
- // Count
1920
- const count = await UserRepo.query()
1921
- .where({ status: 'active' })
1922
- .count();
1923
-
1924
- // Pagination
1925
- const result = await UserRepo.query()
1926
- .where({ status: 'active' })
1927
- .orderBy('created_at', 'desc')
1928
- .paginate(1, 20); // page 1, 20 per page
1929
-
1930
- // result = { data: [...], total: 150, page: 1, perPage: 20, totalPages: 8 }
1931
-
1932
- // Soft delete scopes
1933
- await UserRepo.query().withTrashed().list(); // Include deleted
1934
- await UserRepo.query().onlyTrashed().list(); // Only deleted
1935
-
1936
- // Multi-tenant
1937
- await UserRepo.query().forTenant(tenantId).list();
1938
- ```
1939
-
1940
- `list()`, `first()`, and `paginate()` emit the same `beforeFind` / `afterFind` lifecycle hooks as `findAll` / `findOne` (one `afterFind` per row). `count()` ignores any `.limit()` / `.offset()` on the builder so it returns the full matching row count.
1941
-
1942
- `query().delete()` runs a SQL `DELETE` for matching rows. On models with soft deletes, use `UserRepo.delete(id)` (or equivalent) to set `deleted_at`; the query builder does not convert deletes to soft deletes.
1943
-
1944
- ### Transactions
1945
-
1946
- ```javascript
1947
- await db.transaction(async (trx) => {
1948
- const userRepo = trx.getRepository('User'); // Use model name
1949
- const postRepo = trx.getRepository('Post');
1950
-
1951
- const user = await userRepo.create({ email: 'new@test.com', name: 'New' });
1952
- await postRepo.create({ title: 'First Post', user_id: user.id });
1953
-
1954
- // All changes committed on success
1955
- // Rolled back on error
1956
- });
1957
- ```
1958
-
1959
- **ORM reads inside `db.transaction()` always bypass the query cache** (Knex transaction client), so you never serve stale rows mid-transaction.
1960
-
1961
- ### ORM query cache (optional)
1962
-
1963
- Enable an in-process, tag-based cache for common read paths. Default provider is in-memory (`createMemoryCacheProvider`); you can pass a custom **`provider`** with the same shape (`get`, `set` with optional `tags` / `ttlMs`, `invalidateTags`, `clear`, `getSizeStats`).
1964
-
1965
- **Turn it on:**
1966
-
1967
- ```javascript
1968
- const db = createDatabase({
1969
- client: 'pg',
1970
- connection: process.env.DATABASE_URL,
1971
- models: './models',
1972
- cache: true, // same as { enabled: true, defaultStrategy: 'auto', memory provider }
1973
- // cache: {
1974
- // enabled: true,
1975
- // defaultStrategy: 'auto', // or 'smart'
1976
- // memory: { maxEntries: 50_000, defaultTtlMs: undefined },
1977
- // // provider: myRedisLikeAdapter,
1978
- // },
1979
- });
1980
- ```
1981
-
1982
- When enabled, **`db.cache`** exposes **`purge()`**, **`invalidateTags(string[])`**, **`invalidateModel('ModelName')`**, **`getMetrics()`**, and **`resetMetrics()`**. When the cache is off, **`db.cache`** is **`null`**.
1983
-
1984
- **Per-model opt-in / strategy** (inherits the database **`defaultStrategy`** when set to `true`):
1985
-
1986
- ```javascript
1987
- defineModel({
1988
- name: 'User',
1989
- table: 'users',
1990
- schema: UserSchema,
1991
- cache: true, // use db defaultStrategy
1992
- // cache: 'auto', // invalidate all cached reads for this model on any row change
1993
- // cache: 'smart', // finer-grained tags: PK reads vs list/collection queries
1994
- // cache: { strategy: 'smart' },
1995
- // cache: false, // never cache this model
1996
- });
1997
- ```
1998
-
1999
- **What gets cached:** **`findById`**, **`findOne`**, **`findAll`**, and query builder **`first`**, **`list`**, **`count`**, **`paginate`** — only when the model participates in caching and the read is not on a transaction connection. Some query shapes are never cached (e.g. raw `where` fragments); the layer increments a **`bypassed`** metric for those.
2000
-
2001
- **Invalidation:** Hooks on **`afterCreate`**, **`afterUpdate`**, **`afterDelete`**, and **`afterRestore`** schedule tag invalidation after the Knex transaction commits (when applicable). With **`auto`**, every mutation clears all cache entries tagged for that model. With **`smart`**, creates invalidate collection query tags; updates/deletes target the row’s primary key tag plus collection tags when the record exposes an id (falls back to full-model invalidation if the id is missing). Bulk **`updateWhere`** / query-builder **`update`** / **`delete`** that affect rows call **`invalidateModelAll`** for safety.
2002
-
2003
- **Exports:** **`createMemoryCacheProvider`**, **`OrmCacheLayer`**, and **`createOrmCacheFromConfig`** are available from **`webspresso`** for advanced/testing setups.
2004
-
2005
- ### Migrations
2006
-
2007
- **CLI Commands:**
2008
-
2009
- ```bash
2010
- # Run pending migrations
2011
- webspresso db:migrate
2012
-
2013
- # Rollback last batch
2014
- webspresso db:rollback
2015
-
2016
- # Rollback all
2017
- webspresso db:rollback --all
2018
-
2019
- # Show migration status
2020
- webspresso db:status
2021
-
2022
- # Create empty migration
2023
- webspresso db:make create_posts_table
2024
-
2025
- # Create migration from model (scaffolding)
2026
- webspresso db:make create_users_table --model User
2027
-
2028
- # Scaffold create-table migrations for all models in models/
2029
- webspresso db:scaffold
2030
- webspresso db:scaffold --only User,Post
2031
- webspresso db:scaffold --dry-run
2032
-
2033
- # Admin Panel Setup
2034
- webspresso admin:setup # Create admin_users migration
2035
- webspresso admin:list # List all admin users
2036
- webspresso admin:password # Reset admin password (interactive or -e -p)
2037
- ```
2038
-
2039
- **Admin CLI Commands:**
2040
-
2041
- ```bash
2042
- # Create admin_users table migration
2043
- webspresso admin:setup
2044
-
2045
- # List all admin users
2046
- webspresso admin:list
2047
-
2048
- # Reset admin password (interactive: prompts for email and password)
2049
- webspresso admin:password
2050
-
2051
- # Reset with options
2052
- webspresso admin:password -e admin@example.com -p yeni_sifre123
2053
-
2054
- # Use custom config or environment
2055
- webspresso admin:password -c ./webspresso.db.js -E production
2056
- webspresso admin:list -c ./webspresso.db.js
2057
- ```
2058
-
2059
- > **Note:** Database config is loaded from `webspresso.db.js` or `knexfile.js` in the project root. Run commands from your project directory.
2060
-
2061
- **Database Config File (`webspresso.db.js`):**
2062
-
2063
- ```javascript
2064
- module.exports = {
2065
- client: 'pg', // or 'mysql2', 'better-sqlite3'
2066
- connection: process.env.DATABASE_URL,
2067
- migrations: {
2068
- directory: './migrations',
2069
- tableName: 'knex_migrations',
2070
- },
2071
-
2072
- // Environment overrides
2073
- production: {
2074
- connection: process.env.DATABASE_URL,
2075
- pool: { min: 2, max: 10 },
2076
- },
2077
- };
2078
- ```
2079
-
2080
- **Programmatic API:**
2081
-
2082
- ```javascript
2083
- const db = createDatabase({
2084
- client: 'pg',
2085
- connection: process.env.DATABASE_URL,
2086
- migrations: { directory: './migrations' },
2087
- });
2088
-
2089
- await db.migrate.latest(); // Run pending
2090
- await db.migrate.rollback(); // Rollback last batch
2091
- await db.migrate.rollback({ all: true }); // Rollback all
2092
- const status = await db.migrate.status(); // Get status
2093
- ```
2094
-
2095
- ### Migration Scaffolding
2096
-
2097
- Generate migration from model schema:
2098
-
2099
- ```javascript
2100
- const { scaffoldMigration } = require('webspresso');
2101
-
2102
- const migration = scaffoldMigration(User);
2103
- // Outputs complete migration file content with:
2104
- // - All columns with proper types
2105
- // - Indexes
2106
- // - Foreign key constraints
2107
- // - Up and down functions
2108
- ```
2109
-
2110
- ### Supported Databases
2111
-
2112
- Install the appropriate driver as a peer dependency:
2113
-
2114
- ```bash
2115
- # PostgreSQL
2116
- npm install pg
2117
-
2118
- # MySQL
2119
- npm install mysql2
2120
-
2121
- # SQLite
2122
- npm install better-sqlite3
2123
- ```
2124
-
2125
- ### Design Philosophy
2126
-
2127
- | Boundary | Zod's Job | ORM's Job |
2128
- |----------|-----------|-----------|
2129
- | Schema definition | Type shape, validation rules | Column metadata extraction |
2130
- | Input validation | `.parse()` / `.safeParse()` | Never - pass through to Zod |
2131
- | Query building | N/A | Full ownership |
2132
- | Relation resolution | N/A | Eager loading with batch queries |
2133
- | Timestamps/SoftDelete | N/A | Auto-inject on operations |
2134
-
2135
- **N+1 Prevention:** Relations are always loaded with batch `WHERE IN (...)` queries, never with individual queries per record.
2136
-
2137
- ### Database Seeding
2138
-
2139
- **CLI Command:**
2140
-
2141
- The easiest way to seed your database is using the CLI command:
2142
-
2143
- ```bash
2144
- # Run seeds (requires seeds/index.js)
2145
- webspresso seed
2146
-
2147
- # Setup seed files if they don't exist
2148
- webspresso seed --setup
2149
-
2150
- # Use custom database config
2151
- webspresso seed --config ./custom-db-config.js
2152
-
2153
- # Use different environment
2154
- webspresso seed --env production
2155
- ```
2156
-
2157
- The `webspresso seed` command:
2158
- - Automatically loads all models from `models/` directory
2159
- - Generates fake data based on model schemas
2160
- - Creates 10 records per model by default
2161
- - Uses smart field detection for appropriate fake data
2162
-
2163
- **Manual Setup:**
2164
-
2165
- Generate fake data for testing and development using `@faker-js/faker`:
2166
-
2167
- ```bash
2168
- npm install @faker-js/faker
2169
- ```
2170
-
2171
- **Basic Usage:**
2172
-
2173
- ```javascript
2174
- const { faker } = require('@faker-js/faker');
2175
- const db = createDatabase({ /* config */ });
2176
-
2177
- const seeder = db.seeder(faker);
2178
-
2179
- // Generate a single record
2180
- const user = await seeder.factory('User').create();
2181
-
2182
- // Generate multiple records
2183
- const users = await seeder.factory('User').create(10);
2184
-
2185
- // Generate without saving (for testing)
2186
- const userData = seeder.factory('User').make();
2187
- ```
2188
-
2189
- **Define Factories with Defaults and States:**
2190
-
2191
- ```javascript
2192
- seeder.defineFactory('User', {
2193
- // Default values
2194
- defaults: {
2195
- status: 'pending',
2196
- },
2197
-
2198
- // Custom generators
2199
- generators: {
2200
- username: (f) => f.internet.username().toLowerCase(),
2201
- },
2202
-
2203
- // Named states for variations
2204
- states: {
2205
- admin: { role: 'admin', status: 'active' },
2206
- verified: (f) => ({
2207
- status: 'verified',
2208
- verified_at: f.date.past().toISOString(),
2209
- }),
2210
- },
2211
- });
2212
-
2213
- // Use states
2214
- const admin = await seeder.factory('User').state('admin').create();
2215
- const verified = await seeder.factory('User').state('verified').create();
2216
- ```
2217
-
2218
- **Smart Field Detection:**
2219
-
2220
- The seeder automatically generates appropriate fake data based on column names:
2221
-
2222
- | Field Name Pattern | Generated Data |
2223
- |-------------------|----------------|
2224
- | `email`, `*_email` | Valid email address |
2225
- | `name`, `first_name`, `last_name` | Person names |
2226
- | `username` | Username |
2227
- | `title` | Short sentence |
2228
- | `content`, `body`, `description` | Paragraphs |
2229
- | `slug` | URL-safe slug |
2230
- | `phone`, `tel` | Phone number |
2231
- | `address`, `city`, `country` | Location data |
2232
- | `price`, `amount`, `cost` | Decimal numbers |
2233
- | `*_url`, `avatar`, `image` | URLs |
2234
-
2235
- **Override and Custom Generators:**
2236
-
2237
- ```javascript
2238
- const user = await seeder.factory('User')
2239
- .override({ email: 'test@example.com' })
2240
- .generators({
2241
- code: (f) => `USR-${f.string.alphanumeric(8)}`,
2242
- })
2243
- .create();
2244
- ```
2245
-
2246
- **Batch Seeding:**
2247
-
2248
- ```javascript
2249
- // Seed multiple models at once
2250
- const results = await seeder.run([
2251
- { model: 'Company', count: 5 },
2252
- { model: 'User', count: 20, state: 'active' },
2253
- { model: 'Post', count: 50 },
2254
- ]);
2255
-
2256
- // Access results
2257
- console.log(results.Company); // Array of 5 companies
2258
- console.log(results.User); // Array of 20 users
2259
- ```
2260
-
2261
- **Cleanup:**
2262
-
2263
- ```javascript
2264
- // Truncate specific tables
2265
- await seeder.truncate('User');
2266
- await seeder.truncate(['User', 'Post']);
2267
-
2268
- // Clear all registered model tables
2269
- await seeder.clearAll();
2270
- ```
2271
-
2272
- ### Schema Explorer Plugin
2273
-
2274
- A plugin that exposes ORM schema information via API endpoints. Useful for frontend code generation, documentation, or admin tools.
2275
-
2276
- **Setup:**
2277
-
2278
- ```javascript
2279
- const { createApp, schemaExplorerPlugin } = require('webspresso');
2280
-
2281
- const app = createApp({
2282
- plugins: [
2283
- schemaExplorerPlugin({
2284
- path: '/_schema', // Endpoint path (default: '/_schema')
2285
- enabled: true, // Force enable (default: auto based on NODE_ENV)
2286
- exclude: ['Secret'], // Exclude specific models
2287
- includeColumns: true, // Include column metadata
2288
- includeRelations: true, // Include relation metadata
2289
- includeScopes: true, // Include scope configuration
2290
- authorize: (req) => { // Custom authorization
2291
- return req.headers['x-api-key'] === 'secret';
2292
- },
2293
- }),
2294
- ],
2295
- });
2296
- ```
2297
-
2298
- **Endpoints:**
2299
-
2300
- - `GET /_schema` - List all models
2301
- - `GET /_schema/:modelName` - Get single model details
2302
- - `GET /_schema/openapi` - Export in OpenAPI 3.0 schema format
2303
-
2304
- **Example Response (`GET /_schema`):**
2305
-
2306
- ```json
2307
- {
2308
- "meta": {
2309
- "version": "1.0.0",
2310
- "generatedAt": "2024-01-01T12:00:00.000Z",
2311
- "modelCount": 2
2312
- },
2313
- "models": [
2314
- {
2315
- "name": "User",
2316
- "table": "users",
2317
- "primaryKey": "id",
2318
- "columns": [
2319
- { "name": "id", "type": "bigint", "primary": true, "autoIncrement": true },
2320
- { "name": "email", "type": "string", "unique": true },
2321
- { "name": "company_id", "type": "bigint", "references": "companies" }
2322
- ],
2323
- "relations": [
2324
- { "name": "company", "type": "belongsTo", "relatedModel": "Company", "foreignKey": "company_id" }
2325
- ],
2326
- "scopes": { "softDelete": true, "timestamps": true, "tenant": null }
2327
- }
2328
- ]
2329
- }
2330
- ```
2331
-
2332
- **Plugin API (programmatic usage):**
2333
-
2334
- ```javascript
2335
- const plugin = schemaExplorerPlugin();
2336
-
2337
- // Plugin API can be used by other plugins or in code
2338
- const models = plugin.api.getModels(); // All models
2339
- const user = plugin.api.getModel('User'); // Single model
2340
- const names = plugin.api.getModelNames(); // Model names
2341
- ```
2342
-
2343
- ### REST resources plugin
2344
-
2345
- Registers **REST-style CRUD** routes for ORM models (`GET` list, `GET /:id`, `POST`, `PATCH /:id`, `DELETE /:id`). Relations are eager-loaded only when the client passes **`?include=relation1,relation2`**; loading uses the ORM batch eager loader (no classic N+1 from relation queries). **Nested includes** (e.g. `posts.comments`) are **not** supported—only top-level relation names defined on the model.
2346
-
2347
- **Model opt-in** via `defineModel({ ..., rest: { enabled: true, path: 'optional-segment', allowInclude: ['company'] } })`. If you pass **`models: ['User', 'Post']`** to the plugin, those names are exposed even when `rest.enabled` is false.
2348
-
2349
- **Setup:**
2350
-
2351
- ```javascript
2352
- const { createApp, restResourcePlugin } = require('webspresso');
2353
-
2354
- const { app } = createApp({
2355
- pagesDir: './pages',
2356
- db,
2357
- plugins: [
2358
- restResourcePlugin({
2359
- path: '/api/rest',
2360
- middleware: [], // optional Express handlers (e.g. auth) — `attachDbMiddleware` is applied after these
2361
- models: null, // optional whitelist of model names; when omitted, only `rest.enabled` models are exposed
2362
- excludeModels: [],
2363
- filter: null, // optional (model) => boolean
2364
- }),
2365
- ],
2366
- });
2367
- ```
2368
-
2369
- List query parameters: `page`, `perPage`, `sort`, `order`, `include`, `trashed` (`only` / `include` when the model uses soft delete), plus **equality filters** on real columns (unknown keys are ignored).
2370
-
2371
- ### File upload plugin
2372
-
2373
- Registers **POST** `multipart/form-data` (field name **`file`** by default) and stores the file(s) via a pluggable provider. The framework ships with **`createLocalFileProvider({ destDir, publicBasePath })`** (writes to disk and returns public URL paths). Use **`mimeAllowlist`** / **`extensionAllowlist`**, **`maxBytes`** (default **10 MiB**), **`multiple`** (default **`false`**), and **`maxFiles`** (default **10**) on the server; production apps should prefer an explicit MIME allowlist.
2374
-
2375
- **Setup:**
2376
-
2377
- ```javascript
2378
- const { createApp, uploadPlugin, adminPanelPlugin } = require('webspresso');
2379
-
2380
- const { app } = createApp({
2381
- pagesDir: './pages',
2382
- publicDir: './public',
2383
- db,
2384
- plugins: [
2385
- uploadPlugin({
2386
- path: '/api/upload',
2387
- local: {
2388
- destDir: './public/uploads',
2389
- publicBasePath: '/uploads',
2390
- },
2391
- maxBytes: 10 * 1024 * 1024,
2392
- mimeAllowlist: ['image/jpeg', 'image/png', 'application/pdf'],
2393
- extensionAllowlist: ['jpg', 'jpeg', 'png', 'pdf'],
2394
- middleware: [], // optional Express handlers (e.g. session auth)
2395
- fieldName: 'file',
2396
- multiple: false, // set to true to allow multiple file uploads in a single request
2397
- maxFiles: 10, // max number of files to accept when multiple is true
2398
- }),
2399
- adminPanelPlugin({
2400
- db,
2401
- // uploadUrl optional: defaults to app.get('webspresso.uploadPath') set by uploadPlugin
2402
- }),
2403
- ],
2404
- });
2405
- ```
2406
-
2407
- - **ORM (Single File):** `zdb.file({ maxLength: 2048, nullable: true })` — string column for the stored public URL or path; migrations use `table.string(..., maxLength)`.
2408
- - **ORM (Multiple Files):** `zdb.files({ nullable: true })` — array column that stores a list of file URLs/paths as a JSON field in the database; migrations use `table.json()`.
2409
- - **Admin forms:** columns with `zdb.file()` or `zdb.files()` automatically render a drag-and-drop upload widget (or a manual URL text field when `uploadUrl` is not configured). Multiple file upload fields (`zdb.files()`) allow adding and removing multiple files concurrently and show preview grids. Image URLs show an inline thumbnail preview; use `ui.accept: 'image/*'` for image-only fields. Optional `ui: { label, hint, accept, maxBytes }` on the column customizes the widget. For existing `zdb.string()` columns you can use `admin.customFields: { columnName: { type: 'file-upload' } }` instead of changing the schema type.
2410
- - **Admin:** the panel reads **`settings.uploadUrl`** from the registry (set automatically when `uploadPlugin` is registered **before** `adminPanelPlugin`, or pass **`adminPanelPlugin({ uploadUrl: '/api/upload' })`**). File fields (`type: 'file'`, `'files'` or `customFields` type `file-upload`) POST to that URL with credentials; the saved record stores the returned **`url`** / **`publicUrl`** string (or an array of URLs for `'files'`).
2411
- - **Response:** Single: `{ url, publicUrl, key? }`. Multiple: `[{ url, publicUrl, key? }, ...]`.
2412
- - **Custom storage:** `uploadPlugin({ provider: { async put({ buffer, originalName, mimeType, size, req }) { return { publicUrl: '...' }; } } })`.
2413
-
2414
- **Admin model example** (`zdb.file()` and `zdb.files()` pick up the upload widgets from schema; no extra `customFields` needed):
2415
-
2416
- ```javascript
2417
- const { defineModel, zdb } = require('webspresso');
2418
-
2419
- const Post = defineModel({
2420
- name: 'Post',
2421
- table: 'posts',
2422
- schema: zdb.schema({
2423
- id: zdb.id(),
2424
- title: zdb.string(),
2425
- cover_image: zdb.file({
2426
- maxLength: 2048,
2427
- nullable: true,
2428
- ui: { label: 'Cover', accept: 'image/*', hint: 'JPEG or PNG' },
2429
- }),
2430
- gallery: zdb.files({
2431
- nullable: true,
2432
- ui: { label: 'Gallery Images', accept: 'image/*' },
2433
- }),
2434
- }),
2435
- admin: { enabled: true, label: 'Posts' },
2436
- });
2437
- ```
2438
-
2439
- For a plain string column, use `admin.customFields: { attachment: { type: 'file-upload' } }` instead of `zdb.file()`.
2440
-
2441
- ### Health check plugin
2442
-
2443
- Exposes a lightweight **GET** endpoint for load balancers and orchestrators (Kubernetes, Docker healthcheck, etc.). **Enabled by default** in all environments; set `enabled: false` to turn it off.
2444
-
2445
- **Setup:**
2446
-
2447
- ```javascript
2448
- const { createApp, healthCheckPlugin } = require('webspresso');
2449
-
2450
- const app = createApp({
2451
- plugins: [
2452
- healthCheckPlugin({
2453
- path: '/health', // default
2454
- verbose: true, // timestamp, uptime, NODE_ENV, framework name/version
2455
- authorize: (req) => true, // optional — restrict who can read the endpoint
2456
- checks: async ({ db }) => {
2457
- if (db) await db.knex.raw('select 1');
2458
- return { database: 'ok' };
2459
- },
2460
- }),
2461
- ],
2462
- });
2463
- ```
2464
-
2465
- - **`checks`**: If this function throws, the handler responds with **503** and `{ status: 'unhealthy', error, ... }`. Return a plain object to merge into `checks` on success (e.g. dependency status).
2466
- - Use a **custom `path`** if your app already serves `GET /health` from `pages/`.
2467
-
2468
- ### Swagger / OpenAPI plugin
2469
-
2470
- Serves **OpenAPI 3** for file-based `pages/api` routes and optional [Zod](https://zod.dev) `schema` exports, plus a **Swagger UI** page. Defaults to **development only** (same idea as the schema explorer).
2471
-
2472
- **Setup:**
2473
-
2474
- ```javascript
2475
- const { createApp, swaggerPlugin } = require('webspresso');
2476
-
2477
- const app = createApp({
2478
- plugins: [
2479
- swaggerPlugin({
2480
- path: '/_swagger', // UI: GET /_swagger, spec: GET /_swagger/openapi.json
2481
- enabled: true, // default: true in development, false in production
2482
- title: 'My API', // optional OpenAPI info.title
2483
- serverUrl: 'https://api.example.com', // optional servers[0].url (else BASE_URL or localhost)
2484
- includeOrmSchemas: false, // merge ORM model schemas into components.schemas
2485
- ormExclude: ['Secret'], // when includeOrmSchemas is true
2486
- authorize: (req) => true, // optional gate for both UI and JSON
2487
- }),
2488
- ],
2489
- });
2490
- ```
2491
-
2492
- **Endpoints:**
2493
-
2494
- - `GET /_swagger/openapi.json` — Full OpenAPI document (`paths` from API routes; request/response shapes from exported `schema({ z })` when present).
2495
- - `GET /_swagger` — Swagger UI (loads the JSON above; requires network access for CDN assets).
2496
-
2497
- In production, keep the plugin disabled or protect it with `authorize` / your own middleware.
2498
-
2499
- ### CSRF protection plugin
2500
-
2501
- Protects mutating requests against Cross-Site Request Forgery (CSRF) using the double-submit token pattern. Supports session storage (when `req.session` is available) and automatically falls back to cookie-based storage.
2502
-
2503
- **Setup:**
2504
-
2505
- ```javascript
2506
- const { createApp, csrfPlugin } = require('webspresso');
2507
-
2508
- const app = createApp({
2509
- plugins: [
2510
- csrfPlugin({
2511
- global: true, // Enable globally for all routes
2512
- cookie: true, // Use cookie-based storage (default uses session if present)
2513
- ignorePaths: ['/api/webhook'], // Skip CSRF verification for specific routes
2514
- }),
2515
- ],
2516
- });
2517
- ```
2518
-
2519
- **Route-Specific Middleware:**
2520
-
2521
- If `global: false` (default), you can protect specific routes by applying the named route middleware `csrf` in your route configuration:
2522
-
2523
- ```javascript
2524
- // pages/submit.js
2525
- module.exports = {
2526
- middleware: ['csrf'],
2527
- post(req, res) {
2528
- res.send('Success!');
2529
- }
2530
- };
2531
- ```
2532
-
2533
- **Template Helpers:**
2534
-
2535
- The plugin registers two Nunjucks template helpers:
2536
- - `{{ fsy.csrfToken() }}`: Returns the raw token string (useful for API fetch/headers).
2537
- - `{{ fsy.csrfInput() }}`: Renders a hidden input field: `<input type="hidden" name="_csrf" value="...">`.
2538
-
2539
- ## Development
2540
-
2541
- Native addons (**better-sqlite3**, **bcrypt**, **sharp**) are compiled for your current Node ABI. After switching Node major versions (e.g. nvm, fnm, Volta), run **`npm run rebuild:native`** or a clean install: `rm -rf node_modules && npm ci`. **chokidar** is not ABI-tied like those drivers; if file watching misbehaves, reinstall dependencies. The repo includes [`.nvmrc`](.nvmrc) (Node 20 LTS) as a known-good default for this project.
2542
-
2543
- ```bash
2544
- # Install dependencies
2545
- npm install
2546
-
2547
- # If you changed Node version and see MODULE_VERSION or .node load errors:
2548
- npm run rebuild:native
2549
-
2550
- # Run tests
2551
- npm test
2552
-
2553
- # Run tests in watch mode
2554
- npm run test:watch
2555
-
2556
- # Run tests with coverage
2557
- npm run test:coverage
2558
-
2559
- # Micro-benchmarks (Vitest bench; CI: main push uploads benchmark-baseline artifact, PRs compare against it)
2560
- npm run bench
2561
- # Local baseline + compare: npm run bench:ci:local
2562
- ```
151
+ ---
2563
152
 
2564
- ## License
153
+ ## 📄 License
2565
154
 
2566
- MIT
155
+ MIT © [Webspresso Contributors](LICENSE)