webspresso 0.0.96 → 0.0.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/ADMIN.md +153 -5
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/CLI.md +195 -1
- package/.agents/PLUGINS.md +153 -9
- package/.agents/QUEUE.md +43 -0
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +6 -3
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +31 -17
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/index.js +12 -7
- package/core/orm/model.js +49 -7
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/manager.js +123 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +660 -10
- package/index.js +44 -96
- package/package.json +32 -12
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +124 -31
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +34 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/audit-log/admin-component.js +11 -0
- package/plugins/audit-log/api-handlers.js +34 -0
- package/plugins/audit-log/index.js +129 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +49 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +11 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +30 -8
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +3 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/file-router.js +213 -63
- package/src/helpers.js +958 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/server.js +629 -63
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webspresso-admin-and-plugins
|
|
3
|
+
description: >-
|
|
4
|
+
Extend the Mithril.js Admin Panel SPA and configure official plugins in Webspresso.
|
|
5
|
+
Use when creating custom admin pages (component.js, page.json), custom field renderers,
|
|
6
|
+
dashboard widgets, actions, or configuring plugins like fileManager, xlsx, csv, polar, realtime, csrf, cors, and basicAuth.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Webspresso Admin Panel & Plugins Skill
|
|
10
|
+
|
|
11
|
+
The Admin Panel is a modular Mithril.js SPA mounted at `/_admin` powered by `adminPanelPlugin({ db })`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Custom Admin Pages (`pagesDir` or `registerPageDir`)
|
|
16
|
+
|
|
17
|
+
Folder structure for auto-discovered admin pages:
|
|
18
|
+
```text
|
|
19
|
+
admin/pages/
|
|
20
|
+
└── system-stats/
|
|
21
|
+
├── page.json
|
|
22
|
+
└── component.js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### `page.json`
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"id": "system-stats",
|
|
29
|
+
"label": "System Statistics",
|
|
30
|
+
"icon": "activity",
|
|
31
|
+
"order": 10,
|
|
32
|
+
"layout": true,
|
|
33
|
+
"permission": "admin"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `component.js` (Clean Mithril Component)
|
|
38
|
+
```javascript
|
|
39
|
+
module.exports = {
|
|
40
|
+
oninit(vnode) {
|
|
41
|
+
vnode.state.stats = null;
|
|
42
|
+
vnode.state.loading = true;
|
|
43
|
+
m.request({ method: 'GET', url: '/_admin/api/custom/stats' }).then((res) => {
|
|
44
|
+
vnode.state.stats = res;
|
|
45
|
+
vnode.state.loading = false;
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
view(vnode) {
|
|
49
|
+
if (vnode.state.loading) return m('.p-6', 'Loading stats...');
|
|
50
|
+
return m('.space-y-4', [
|
|
51
|
+
m('h2.text-xl.font-bold', 'System Overview'),
|
|
52
|
+
m('.grid.grid-cols-3.gap-4', [
|
|
53
|
+
m('.card.p-4.border.rounded', [
|
|
54
|
+
m('.text-sm.text-gray-500', 'Total Users'),
|
|
55
|
+
m('.text-2xl.font-semibold', vnode.state.stats.usersCount),
|
|
56
|
+
]),
|
|
57
|
+
]),
|
|
58
|
+
]);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. Admin Panel Extension Points
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// Register custom widget in dashboard
|
|
69
|
+
registry.registerWidget('recent-signups', {
|
|
70
|
+
label: 'Recent Signups',
|
|
71
|
+
component: require('./widgets/recent-signups'),
|
|
72
|
+
width: 'half', // 'full' | 'half' | 'third'
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Register single record action button
|
|
76
|
+
registry.registerAction('User', {
|
|
77
|
+
name: 'impersonate',
|
|
78
|
+
label: 'Impersonate',
|
|
79
|
+
icon: 'user-check',
|
|
80
|
+
handler: async (record) => { ... },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Register custom field display/edit renderer
|
|
84
|
+
registry.registerFieldRenderer('color-picker', {
|
|
85
|
+
display: (val) => m('.w-6.h-6.rounded', { style: { backgroundColor: val } }),
|
|
86
|
+
edit: (val, onChange) => m('input[type=color]', { value: val, onchange: (e) => onChange(e.target.value) }),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 3. Official Plugin Catalog & Usage
|
|
93
|
+
|
|
94
|
+
In `createApp({ plugins: [...] })`:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const {
|
|
98
|
+
adminPanelPlugin,
|
|
99
|
+
fileManagerPlugin,
|
|
100
|
+
xlsxPlugin,
|
|
101
|
+
csvPlugin,
|
|
102
|
+
basicAuthPlugin,
|
|
103
|
+
corsPlugin,
|
|
104
|
+
csrfPlugin,
|
|
105
|
+
realtimePlugin,
|
|
106
|
+
polarPlugin,
|
|
107
|
+
} = require('webspresso/plugins');
|
|
108
|
+
|
|
109
|
+
createApp({
|
|
110
|
+
plugins: [
|
|
111
|
+
adminPanelPlugin({ db }),
|
|
112
|
+
fileManagerPlugin({ baseDir: './public/uploads', publicBasePath: '/uploads' }),
|
|
113
|
+
xlsxPlugin(),
|
|
114
|
+
csvPlugin(),
|
|
115
|
+
basicAuthPlugin({ users: { admin: process.env.ADMIN_BASIC_AUTH_PASS }, routes: ['/metrics'] }),
|
|
116
|
+
corsPlugin({ origin: ['https://example.com'], credentials: true }),
|
|
117
|
+
csrfPlugin({ cookie: true }),
|
|
118
|
+
realtimePlugin({ adapter: 'websocket' }),
|
|
119
|
+
polarPlugin({ apiKey: process.env.POLAR_ACCESS_TOKEN }),
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webspresso-framework
|
|
3
|
+
description: >-
|
|
4
|
+
Core workflow and guide for Webspresso, a lightweight, zero-dependency-sprawl full-stack
|
|
5
|
+
Node.js SSR and API framework. Use when scaffolding, architecting, building, or debugging
|
|
6
|
+
Webspresso applications, configuring plugins, dual auth, response compression, graceful shutdown,
|
|
7
|
+
and error boundaries.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Webspresso Framework Master Guide
|
|
11
|
+
|
|
12
|
+
Webspresso is a lightweight, zero-dependency-sprawl full-stack Node.js SSR & API framework featuring Next.js/Nuxt-style file-based routing, an intuitive Knex-based ORM, a modular plugin ecosystem, and a customizable Mithril.js SPA Admin Panel.
|
|
13
|
+
|
|
14
|
+
## 🛡️ 4-Step Mandatory Agent Workflow Protocol
|
|
15
|
+
|
|
16
|
+
1. **Discover & Triage**: Identify relevant topic guides (`.agents/`) and inspect existing files in `pages/api/`, `models/`, `services/`.
|
|
17
|
+
2. **Contract Adherence**: Export `{ schema, middleware, handler }` for APIs, `defineModel` for ORM, `defineService` for services.
|
|
18
|
+
3. **Zero External Dependencies**: Core functionality relies strictly on native Node.js modules (`crypto`, `path`, `fs`, `zlib`, `events`). NEVER install ad-hoc packages for JWT, passport, express-session, or custom routers.
|
|
19
|
+
4. **Targeted Verification**: Run specific Vitest unit tests (`npx vitest run tests/...`) to ensure zero regressions.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## ⚖️ Golden Rules & Anti-Patterns
|
|
24
|
+
|
|
25
|
+
| Category | ❌ NEVER DO THIS (Anti-Pattern) | ✅ ALWAYS DO THIS (Webspresso Way) |
|
|
26
|
+
| :--- | :--- | :--- |
|
|
27
|
+
| **Routing** | `routes/index.js`, `express.Router()`, `app.get('/api/users')` | `pages/api/users.get.js` exporting `{ schema, handler }` |
|
|
28
|
+
| **Database** | `db('users').where({ id }).select()` (Raw Knex) | `db.getRepository('User').findById(id)` |
|
|
29
|
+
| **Business Logic** | `controllers/UserController.js` | `services/user/get.js` → `ctx.service('user.get', input)` |
|
|
30
|
+
| **Auth** | `npm i jsonwebtoken passport express-session` | Built-in `req.auth`, `core/auth`, `middleware: ['jwt']` |
|
|
31
|
+
| **SSR Rendering**| `res.render('profile.njk', data)` inside handlers | Companion loader `pages/profile.js` exporting `async function load()` |
|
|
32
|
+
| **Exceptions** | `res.status(404).json({ error: 'Not found' })` | `throw new NotFoundError('User not found')` |
|
|
33
|
+
| **Background** | `setTimeout(...)`, `npm i bullmq` | `jobs/user/welcome.js` → `req.queue.dispatch('user.welcome', data)` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🚀 CLI Commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
webspresso dev # Start dev server with hot reload
|
|
41
|
+
webspresso start # Start production server
|
|
42
|
+
webspresso doctor # Run system sanity checks
|
|
43
|
+
webspresso db:migrate # Run pending Knex migrations
|
|
44
|
+
webspresso db:seed # Run database seeders
|
|
45
|
+
webspresso agents:init # Scaffold AI Agent rules and skills
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 🏗️ Core Architecture Overview
|
|
51
|
+
|
|
52
|
+
- **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
|
|
53
|
+
- **`services/`**: Auto-discovered domain services mapping to dot-separated names (`'user.create'`).
|
|
54
|
+
- **`models/`**: Schema-driven ORM models with `zdb` definitions, repositories, relations, scopes, and query cache.
|
|
55
|
+
- **`plugins/`**: Modular extension ecosystem (Admin Panel, Media Library, Polar Billing, CSV/XLSX, Realtime, CSRF, CORS, Basic Auth).
|
|
56
|
+
- **`views/`**: Nunjucks layouts, partials, and `fsy` template helpers.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webspresso-orm-and-database
|
|
3
|
+
description: >-
|
|
4
|
+
Define models, run queries, handle transactions, manage relations, and scaffold migrations in
|
|
5
|
+
Webspresso ORM. Use when writing database models (defineModel), using zdb schema builders,
|
|
6
|
+
querying repositories (db.getRepository), configuring query cache, soft deletes, and ambient transactions.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Webspresso ORM & Database Skill
|
|
10
|
+
|
|
11
|
+
⚠️ **CRITICAL ANTI-PATTERN:** Never write raw Knex table queries (e.g. `db('users')` or `knex('users')`) in application code. Always use Repositories (`db.getRepository('ModelName')`).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Defining Models (`models/*.js`)
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
const { defineModel, zdb } = require('webspresso');
|
|
19
|
+
|
|
20
|
+
module.exports = defineModel({
|
|
21
|
+
name: 'Product',
|
|
22
|
+
table: 'products',
|
|
23
|
+
primaryKey: 'id',
|
|
24
|
+
|
|
25
|
+
schema: zdb.object({
|
|
26
|
+
id: zdb.id(),
|
|
27
|
+
name: zdb.string().min(1).max(255),
|
|
28
|
+
slug: zdb.string().unique(),
|
|
29
|
+
price: zdb.decimal({ precision: 10, scale: 2 }),
|
|
30
|
+
in_stock: zdb.boolean().default(true),
|
|
31
|
+
metadata: zdb.json().optional(),
|
|
32
|
+
user_id: zdb.foreignKey('users.id'),
|
|
33
|
+
created_at: zdb.timestamp().autoCreate(),
|
|
34
|
+
updated_at: zdb.timestamp().autoUpdate(),
|
|
35
|
+
deleted_at: zdb.timestamp().nullable(),
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
relations: {
|
|
39
|
+
user: {
|
|
40
|
+
type: 'belongsTo',
|
|
41
|
+
model: 'User',
|
|
42
|
+
foreignKey: 'user_id',
|
|
43
|
+
},
|
|
44
|
+
reviews: {
|
|
45
|
+
type: 'hasMany',
|
|
46
|
+
model: 'Review',
|
|
47
|
+
foreignKey: 'product_id',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
scopes: {
|
|
52
|
+
softDelete: true, // enables deleted_at filtering automatically
|
|
53
|
+
active: (query) => query.where('in_stock', true),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
hidden: ['metadata.internal_notes'], // stripped from output
|
|
57
|
+
|
|
58
|
+
cache: 'auto', // query caching ('auto' | 'smart' | true)
|
|
59
|
+
|
|
60
|
+
admin: {
|
|
61
|
+
enabled: true,
|
|
62
|
+
label: 'Products',
|
|
63
|
+
icon: 'package',
|
|
64
|
+
columns: ['name', 'price', 'in_stock', 'created_at'],
|
|
65
|
+
search: ['name', 'slug'],
|
|
66
|
+
filters: ['in_stock', 'user_id'],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. Repositories API
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const repo = db.getRepository('Product');
|
|
77
|
+
|
|
78
|
+
// Single Record Queries
|
|
79
|
+
const item = await repo.findById(id);
|
|
80
|
+
const item = await repo.findOne({ slug: 'espresso-blend' });
|
|
81
|
+
|
|
82
|
+
// Multiple Records & Filtering
|
|
83
|
+
const activeItems = await repo.find({ in_stock: true });
|
|
84
|
+
|
|
85
|
+
// Insert & Update (auto-validates with schema & triggers lifecycle hooks)
|
|
86
|
+
const created = await repo.create({
|
|
87
|
+
name: 'Espresso Blend',
|
|
88
|
+
slug: 'espresso-blend',
|
|
89
|
+
price: 14.99,
|
|
90
|
+
user_id: user.id,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const updated = await repo.update(created.id, { price: 16.99 });
|
|
94
|
+
|
|
95
|
+
// Deletion (respects softDelete scope)
|
|
96
|
+
await repo.delete(id);
|
|
97
|
+
|
|
98
|
+
// Fluent Query Builder with Eager Loading & Pagination
|
|
99
|
+
const results = await repo.query()
|
|
100
|
+
.where('price', '>=', 10)
|
|
101
|
+
.include('user', 'reviews')
|
|
102
|
+
.orderBy('created_at', 'desc')
|
|
103
|
+
.paginate({ page: 1, limit: 20 });
|
|
104
|
+
// Returns: { data: [...], pagination: { total, page, limit, totalPages } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 3. Ambient Transactions (`AsyncLocalStorage`)
|
|
110
|
+
|
|
111
|
+
Inside `db.transaction()` or services declared with `transaction: true`, all repository operations automatically bind to the ambient transaction without manual `trx` passing:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
await db.transaction(async () => {
|
|
115
|
+
const user = await db.getRepository('User').create(userData);
|
|
116
|
+
const account = await db.getRepository('Account').create({ userId: user.id, balance: 100 });
|
|
117
|
+
// Automatically commits if promise resolves, or rolls back if an exception is thrown
|
|
118
|
+
});
|
|
119
|
+
```
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webspresso-routing-and-api
|
|
3
|
+
description: >-
|
|
4
|
+
Build and declare file-based API endpoints and SSR pages in Webspresso using defineApi, definePage,
|
|
5
|
+
and defineModule. Use when writing route handlers, data loaders (load()), parameter brackets ([id]),
|
|
6
|
+
Zod input validation, response streaming, and route middleware.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Webspresso Routing & API Skill
|
|
10
|
+
|
|
11
|
+
Webspresso uses strictly file-based routing. Never use `express.Router()`, `app.get()`, or a `routes/` directory.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. File-Based API Endpoints (`pages/api/...`)
|
|
16
|
+
|
|
17
|
+
All API routes live in `pages/api/` and use HTTP method extensions:
|
|
18
|
+
- `pages/api/notes.get.js` → `GET /api/notes`
|
|
19
|
+
- `pages/api/notes.post.js` → `POST /api/notes`
|
|
20
|
+
- `pages/api/notes/[id].get.js` → `GET /api/notes/:id`
|
|
21
|
+
- `pages/api/notes/[id].delete.js` → `DELETE /api/notes/:id`
|
|
22
|
+
|
|
23
|
+
### Standard `defineApi` Contract
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
const { defineApi } = require('webspresso');
|
|
27
|
+
|
|
28
|
+
module.exports = defineApi({
|
|
29
|
+
schema: ({ z }) => ({
|
|
30
|
+
body: z.object({
|
|
31
|
+
title: z.string().min(1, 'Title is required'),
|
|
32
|
+
content: z.string().optional(),
|
|
33
|
+
}),
|
|
34
|
+
params: z.object({
|
|
35
|
+
id: z.string().optional(),
|
|
36
|
+
}),
|
|
37
|
+
query: z.object({
|
|
38
|
+
filter: z.string().optional(),
|
|
39
|
+
}),
|
|
40
|
+
}),
|
|
41
|
+
middleware: ['auth'], // optional named middleware string or array of middlewares
|
|
42
|
+
handler: async (req, res, ctx) => {
|
|
43
|
+
// 1. Validated input is automatically available in req.input
|
|
44
|
+
const { title, content } = req.input.body;
|
|
45
|
+
|
|
46
|
+
// 2. Repositories and Services are injected
|
|
47
|
+
const note = await req.db.getRepository('Note').create({ title, content });
|
|
48
|
+
|
|
49
|
+
// 3. Return response
|
|
50
|
+
return res.status(201).json(note);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 2. SSR HTML Pages & Data Loaders (`pages/...`)
|
|
58
|
+
|
|
59
|
+
- **Template**: `pages/products/[id].njk`
|
|
60
|
+
- **Companion Loader**: `pages/products/[id].js`
|
|
61
|
+
|
|
62
|
+
### Companion Loader (`definePage`)
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
const { definePage } = require('webspresso');
|
|
66
|
+
|
|
67
|
+
module.exports = definePage({
|
|
68
|
+
middleware: ['auth'], // optional
|
|
69
|
+
head: {
|
|
70
|
+
title: 'Product Details',
|
|
71
|
+
},
|
|
72
|
+
load: async ({ req, res, db, ctx }) => {
|
|
73
|
+
const product = await db.getRepository('Product').findById(req.params.id);
|
|
74
|
+
if (!product) {
|
|
75
|
+
throw new ctx.errors.NotFoundError('Product not found');
|
|
76
|
+
}
|
|
77
|
+
return { product, title: product.name };
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Template (`.njk`)
|
|
83
|
+
|
|
84
|
+
```nunjucks
|
|
85
|
+
{% extends "layout.njk" %}
|
|
86
|
+
|
|
87
|
+
{% block content %}
|
|
88
|
+
<div class="product-page">
|
|
89
|
+
<h1>{{ product.name }}</h1>
|
|
90
|
+
<p class="price">{{ fsy.formatCurrency(product.price) }}</p>
|
|
91
|
+
<p>{{ product.description }}</p>
|
|
92
|
+
</div>
|
|
93
|
+
{% endblock %}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 3. Modular Domain Architecture (`defineModule`)
|
|
99
|
+
|
|
100
|
+
Encapsulate feature domains inside `src/modules/` or `modules/`:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
const { defineModule } = require('webspresso');
|
|
104
|
+
|
|
105
|
+
module.exports = defineModule({
|
|
106
|
+
name: 'billing',
|
|
107
|
+
pages: './pages', // auto-discovers pages under /billing/*
|
|
108
|
+
api: './api', // auto-discovers API under /api/billing/*
|
|
109
|
+
services: './services', // auto-discovers services under 'billing.*'
|
|
110
|
+
middlewares: {
|
|
111
|
+
paidOnly: (req, res, next) => { ... },
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webspresso-services
|
|
3
|
+
description: >-
|
|
4
|
+
Encapsulate business logic, validations, transactions, and authorizations in Webspresso services.
|
|
5
|
+
Use when writing service files (services/domain/action.js), defining schemas (defineService),
|
|
6
|
+
configuring transaction: true, calling ctx.service() or req.service(), and adding RBAC guards.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Webspresso Services Layer Skill
|
|
10
|
+
|
|
11
|
+
⚠️ **CRITICAL ANTI-PATTERN:** Never create a `controllers/` folder or controller classes. Encapsulate multi-step business logic in `services/` and invoke via `ctx.service()` or `req.service()`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. File-Based Auto-Discovery (`services/`)
|
|
16
|
+
|
|
17
|
+
Files in `services/` map directly to dot-separated service names:
|
|
18
|
+
- `services/user/create.js` → `'user.create'`
|
|
19
|
+
- `services/order/checkout.js` → `'order.checkout'`
|
|
20
|
+
- `services/billing/process-refund.js` → `'billing.process-refund'` (and `'billing.processRefund'`)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 2. Defining Services (`defineService`)
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
const { defineService } = require('webspresso');
|
|
28
|
+
|
|
29
|
+
module.exports = defineService({
|
|
30
|
+
schema: ({ z }) => z.object({
|
|
31
|
+
userId: z.number().int().positive(),
|
|
32
|
+
amount: z.number().positive(),
|
|
33
|
+
reason: z.string().min(3),
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
// Optional Declarative Auth / Role Guard
|
|
37
|
+
auth: ['admin', 'manager'], // or boolean true, or (ctx, input) => boolean
|
|
38
|
+
|
|
39
|
+
// Automatic ACID Transaction Propagation
|
|
40
|
+
transaction: true,
|
|
41
|
+
|
|
42
|
+
// Optional Service Result Cache
|
|
43
|
+
cache: {
|
|
44
|
+
ttl: 300, // seconds
|
|
45
|
+
key: (input) => `refund:${input.userId}:${input.amount}`,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// Execution Handler
|
|
49
|
+
handler: async (input, ctx) => {
|
|
50
|
+
// ctx.db, ctx.service, ctx.user, ctx.logger are automatically injected
|
|
51
|
+
const userRepo = ctx.db.getRepository('User');
|
|
52
|
+
const accountRepo = ctx.db.getRepository('Account');
|
|
53
|
+
|
|
54
|
+
const user = await userRepo.findById(input.userId);
|
|
55
|
+
if (!user) {
|
|
56
|
+
throw new ctx.errors.NotFoundError('User not found');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const account = await accountRepo.findOne({ userId: user.id });
|
|
60
|
+
const updatedAccount = await accountRepo.update(account.id, {
|
|
61
|
+
balance: account.balance + input.amount,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Nested service call shares the same ambient transaction automatically
|
|
65
|
+
await ctx.service('audit.log', {
|
|
66
|
+
action: 'refund.processed',
|
|
67
|
+
userId: user.id,
|
|
68
|
+
amount: input.amount,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
balance: updatedAccount.balance,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Invoking Services
|
|
82
|
+
|
|
83
|
+
From SSR Companion Loaders:
|
|
84
|
+
```javascript
|
|
85
|
+
module.exports = definePage({
|
|
86
|
+
load: async ({ ctx }) => {
|
|
87
|
+
const data = await ctx.service('order.summary', { userId: ctx.user.id });
|
|
88
|
+
return { data };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
From API Route Handlers:
|
|
94
|
+
```javascript
|
|
95
|
+
module.exports = defineApi({
|
|
96
|
+
handler: async (req, res) => {
|
|
97
|
+
const result = await req.service('order.checkout', req.input.body);
|
|
98
|
+
return res.status(200).json(result);
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add esbuild Command
|
|
3
|
+
* Add esbuild client bundling and incremental watch to the project
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
function registerCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('add esbuild')
|
|
12
|
+
.alias('add:esbuild')
|
|
13
|
+
.description('Add esbuild client bundling and incremental watch to the project')
|
|
14
|
+
.action(async () => {
|
|
15
|
+
if (!fs.existsSync('package.json')) {
|
|
16
|
+
console.error('❌ Not a Webspresso project! Run this command in your project directory.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log('\n⚡ Adding esbuild client bundler to your project...\n');
|
|
21
|
+
|
|
22
|
+
// Read package.json
|
|
23
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
24
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
25
|
+
packageJson.devDependencies['esbuild'] = '^0.25.0';
|
|
26
|
+
|
|
27
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
28
|
+
packageJson.scripts['build:js'] = 'esbuild src/client/main.js --bundle --outdir=public/js --minify --sourcemap';
|
|
29
|
+
packageJson.scripts['watch:js'] = 'esbuild src/client/main.js --bundle --outdir=public/js --sourcemap --watch';
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2) + '\n');
|
|
32
|
+
console.log('✅ Updated package.json (esbuild devDependency and build:js / watch:js scripts)');
|
|
33
|
+
|
|
34
|
+
// Create src/client directory
|
|
35
|
+
if (!fs.existsSync('src/client')) {
|
|
36
|
+
fs.mkdirSync('src/client', { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create src/client/main.js if not exists
|
|
40
|
+
const mainJsPath = path.join('src', 'client', 'main.js');
|
|
41
|
+
if (!fs.existsSync(mainJsPath)) {
|
|
42
|
+
const clientStarterCode = `// Client-side JavaScript entry point
|
|
43
|
+
console.log('⚡ Webspresso client script loaded');
|
|
44
|
+
|
|
45
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
46
|
+
// Client interactivity
|
|
47
|
+
});
|
|
48
|
+
`;
|
|
49
|
+
fs.writeFileSync(mainJsPath, clientStarterCode);
|
|
50
|
+
console.log('✅ Created src/client/main.js');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create public/js directory
|
|
54
|
+
if (!fs.existsSync(path.join('public', 'js'))) {
|
|
55
|
+
fs.mkdirSync(path.join('public', 'js'), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Update layout.njk to include script if not present
|
|
59
|
+
const layoutPath = path.join('views', 'layout.njk');
|
|
60
|
+
if (fs.existsSync(layoutPath)) {
|
|
61
|
+
let layoutContent = fs.readFileSync(layoutPath, 'utf-8');
|
|
62
|
+
if (!layoutContent.includes('/js/main.js')) {
|
|
63
|
+
layoutContent = layoutContent.replace(
|
|
64
|
+
/(<\/head>)/,
|
|
65
|
+
' <script defer src="/js/main.js"></script>\n$1'
|
|
66
|
+
);
|
|
67
|
+
fs.writeFileSync(layoutPath, layoutContent);
|
|
68
|
+
console.log('✅ Updated views/layout.njk (<script defer src="/js/main.js"></script>)');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('\n✅ esbuild setup complete!\n');
|
|
73
|
+
console.log('Next steps:');
|
|
74
|
+
console.log(' npm install');
|
|
75
|
+
console.log(' npm run build:js');
|
|
76
|
+
console.log(' npm run dev\n');
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { registerCommand };
|